/** * Shared schema-HTML builders for the media/discriminator family (#293 canon * #8). * * Canon #8 gives ten node types (youtube/video/audio/drawio/excalidraw — * image-form; pdf/attachment/embed — link-form; pageEmbed/transclusionReference * — standalone) a readable markdown TOP-LEVEL form (`![](src)`/`[text](src)`/a * bare comment) plus a discriminator `` comment. But TWO other * paths still need the RAW SCHEMA-HTML form of each node: * * 1. The serializer's raw-HTML/columns path (`blockToHtml`): a comment node is * dropped by the DOM parse stage that reads a raw-HTML block back, so inside * a column/cell these nodes MUST stay schema HTML or they vanish (data loss). * 2. The importer's `applyCommentDirectives`: to materialize the discriminator * comment it rebuilds the SAME schema element the raw-HTML path emits, then * swaps it in for the ``/``/comment. * * Keeping these builders in ONE module means the serializer's raw-HTML path and * the importer's materialization can never drift: both call the same function. * Each builder reproduces BYTE-FOR-BYTE the schema HTML the top-level * `processNode` cases previously returned (so existing columns/raw-HTML goldens * stay green), and each output round-trips through the matching schema parseHTML * in docmost-schema.ts. */ /** * Escape a value interpolated into an HTML double-quoted attribute value. * Identical semantics to markdown-converter's `escapeAttr`: escape ONLY `&` and * `"` (idempotent; parse5 decodes both back). `<`/`>`/`'` are deliberately left * alone so values never accumulate escapes across round-trips. */ const escapeAttr = (value: unknown): string => String(value).replace(/&/g, "&").replace(/"/g, """); /** * Uploaded `