@mteditor/renderer-web 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ var renderContent = (doc, options) => {
24
24
  });
25
25
  return {
26
26
  output: serialized.output,
27
- // §12.2:只有调用方**显式**要求关闭时才丢弃报告
27
+ // 只有调用方**显式**要求关闭时才丢弃报告
28
28
  report: options.report === false ? [] : serialized.report,
29
29
  stats: serialized.stats
30
30
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/options.ts","../src/container.ts","../src/render-content.ts","../src/render-to-string.ts","../src/render-to-dom.ts"],"names":[],"mappings":";;;AAOO,IAAM,gBAAA,GAAmB;AAGzB,IAAM,aAAA,GAAgB;ACiBtB,IAAM,qBAAA,GAAwB,CAAC,OAAA,KAAgD;AACpF,EAAA,MAAM,QAA2B,EAAC;AAClC,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,gBAAA;AACvC,EAAA,IAAI,SAAA,CAAU,SAAS,CAAA,EAAG,KAAA,CAAM,KAAK,CAAC,OAAA,EAAS,SAAS,CAAC,CAAA;AACzD,EAAA,IAAI,OAAA,CAAQ,UAAU,MAAA,EAAW,KAAA,CAAM,KAAK,CAAC,aAAA,EAAe,OAAA,CAAQ,KAAK,CAAC,CAAA;AAC1E,EAAA,OAAO,KAAA;AACT;AAaO,IAAM,sBAAA,GAAyB,CAAC,OAAA,KACrC,qBAAA,CAAsB,OAAO,CAAA,CAC1B,GAAA,CAAI,CAAC,CAAC,IAAA,EAAM,KAAK,CAAA,KAAM,CAAA,CAAA,EAAI,IAAI,CAAA,EAAA,EAAK,UAAA,CAAW,KAAK,CAAC,CAAA,CAAA,CAAG,CAAA,CACxD,IAAA,CAAK,EAAE;AAQL,IAAM,mBAAA,GAAsB,CAAC,OAAA,EAAkB,OAAA,KAAmC;AACvF,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,KAAK,CAAA,IAAK,qBAAA,CAAsB,OAAO,CAAA,EAAG;AAC1D,IAAA,OAAA,CAAQ,YAAA,CAAa,MAAM,KAAK,CAAA;AAAA,EAClC;AACF;AAiBO,IAAM,aAAA,GAAgB,CAAC,MAAA,EAAgB,OAAA,KAC5C,OAAO,sBAAA,CAAuB,OAAO,CAAC,CAAA,CAAA,EAAI,MAAM,CAAA,MAAA;AC9C3C,IAAM,aAAA,GAAgB,CAAC,GAAA,EAAiB,OAAA,KAAgD;AAC7F,EAAA,MAAM,UAAA,GAAa,wBAAwB,GAAA,EAAK;AAAA,IAC9C,aAAa,OAAA,CAAQ,WAAA;AAAA,IACrB,QAAQ,OAAA,CAAQ;AAAA,GACjB,CAAA;AAED,EAAA,OAAO;AAAA,IACL,QAAQ,UAAA,CAAW,MAAA;AAAA;AAAA,IAEnB,QAAQ,OAAA,CAAQ,MAAA,KAAW,KAAA,GAAQ,KAAK,UAAA,CAAW,MAAA;AAAA,IACnD,OAAO,UAAA,CAAW;AAAA,GACpB;AACF,CAAA;;;ACZO,IAAM,cAAA,GAAiB,CAAC,GAAA,EAAiB,OAAA,GAA2B,EAAC,KAAsB;AAChG,EAAA,MAAM,OAAA,GAAU,aAAA,CAAc,GAAA,EAAK,OAAO,CAAA;AAC1C,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,aAAA,CAAc,OAAA,CAAQ,MAAA,EAAQ,OAAO,CAAA;AAAA,IAC3C,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,OAAO,OAAA,CAAQ;AAAA,GACjB;AACF;;;ACTO,IAAM,cAAc,CACzB,GAAA,EACA,SAAA,EACA,OAAA,GAA2B,EAAC,KACN;AACtB,EAAA,MAAM,OAAA,GAAU,aAAA,CAAc,GAAA,EAAK,OAAO,CAAA;AAC1C,EAAA,MAAM,OAAA,GAAU,SAAA,CAAU,aAAA,CAAc,aAAA,CAAc,KAAK,CAAA;AAE3D,EAAA,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAIpC,EAAA,OAAA,CAAQ,YAAY,OAAA,CAAQ,MAAA;AAE5B,EAAA,SAAA,CAAU,gBAAgB,OAAO,CAAA;AAEjC,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,aAAA,CAAc,OAAA,CAAQ,MAAA,EAAQ,OAAO,CAAA;AAAA,IAC3C,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,OAAO,OAAA,CAAQ,KAAA;AAAA,IACf;AAAA,GACF;AACF","file":"index.js","sourcesContent":["/**\n * `@mteditor/renderer-web` 的公开选项与结果类型(`docs/decisions/ADR-0021`)。\n */\n\nimport type { MtDegradationEntry, MtPreRenderedIndex, MtSerializeStats } from '@mteditor/document'\n\n/** 内容容器类名 —— 与 `@mteditor/theme-default` 的 `.mt-content` 约定一致(AGENTS.md §7.4) */\nexport const MT_CONTENT_CLASS = 'mt-content'\n\n/** 主题属性名 —— 主题层只认这一个属性(ADR-0007) */\nexport const MT_THEME_ATTR = 'data-mt-theme'\n\n/** 主题取值;不开放任意字符串,避免写出主题层无法识别的属性值 */\nexport type MtTheme = 'light' | 'dark'\n\n/** 渲染选项;全部字段可选 */\nexport interface MtRenderOptions {\n /**\n * 容器的**完整** `class` 值,缺省 `'mt-content'`。\n *\n * 需要追加自己的类名时请一并写出,例如 `'mt-content article-body'`;\n * 传空字符串则完全不输出 `class` 属性(此时将失去主题排版样式)。\n */\n className?: string\n /**\n * 主题。**缺省不写 `data-mt-theme`**,跟随宿主(AGENTS.md §7.4 的主题模型)。\n *\n * 只在需要**局部覆盖**时显式传入,例如邮件预览固定亮色。\n */\n theme?: MtTheme\n /** 预渲染产物索引(公式 SVG / 代码高亮,ADR-0012、ADR-0013) */\n preRendered?: MtPreRenderedIndex\n /** 缩进层级,缺省 `0`;仅影响 HTML 可读性,不参与语义 */\n indent?: number\n /** 是否返回降级报告,缺省 `true`。**置 `false` 等于主动放弃降级信息**(§12.2) */\n report?: boolean\n}\n\n/** 渲染结果(`renderToString` 的返回值) */\nexport interface MtRenderResult {\n /** 完整 HTML,**含外层容器** */\n html: string\n /** 降级报告;`report: false` 时为空数组 */\n report: MtDegradationEntry[]\n /** 序列化统计(**不含**外层容器,即 `document` 序列化器的原始统计) */\n stats: MtSerializeStats\n}\n\n/** DOM 渲染结果(`renderToDOM` 的返回值) */\nexport interface MtDomRenderResult extends MtRenderResult {\n /** 实际挂载进容器的元素 */\n element: HTMLElement\n}\n","/**\n * 渲染容器的属性装配(`docs/decisions/ADR-0021`)。\n *\n * 容器属性有**两条出口**:字符串出口(`renderToString`)与真实元素出口(`renderToDOM`)。\n * 两者必须产出同一组属性,因此都从本模块的 `resolveContainerAttrs` 取值——\n * 这是「SSR 字符串与客户端 DOM 一致」的实现基础。\n */\n\nimport { escapeAttr } from '@mteditor/document'\n\nimport { MT_CONTENT_CLASS, MT_THEME_ATTR, type MtRenderOptions } from './options'\n\n/** 属性对:`[属性名, 属性值]`,值必为字符串(无「缺省」概念,缺省即不产出该对) */\ntype MtContainerAttr = readonly [name: string, value: string]\n\n/**\n * 解析容器应当产出的属性对(顺序固定:`class` 在前)。\n *\n * @param options 渲染选项\n * @returns 属性对列表;可能为空数组\n *\n * @example\n * ```ts\n * resolveContainerAttrs({ className: '' }) // []\n * resolveContainerAttrs({ theme: 'dark' }) // [['class', 'mt-content'], ['data-mt-theme', 'dark']]\n * ```\n */\nexport const resolveContainerAttrs = (options: MtRenderOptions): MtContainerAttr[] => {\n const attrs: MtContainerAttr[] = []\n const className = options.className ?? MT_CONTENT_CLASS\n if (className.length > 0) attrs.push(['class', className])\n if (options.theme !== undefined) attrs.push([MT_THEME_ATTR, options.theme])\n return attrs\n}\n\n/**\n * 把容器属性序列化为 HTML 属性串。\n *\n * @param options 渲染选项\n * @returns 形如 ` class=\"mt-content\"` 的字符串;无属性时为空串\n *\n * @example\n * ```ts\n * containerAttrsToString({}) // ' class=\"mt-content\"'\n * ```\n */\nexport const containerAttrsToString = (options: MtRenderOptions): string =>\n resolveContainerAttrs(options)\n .map(([name, value]) => ` ${name}=\"${escapeAttr(value)}\"`)\n .join('')\n\n/**\n * 把容器属性写到真实元素上。\n *\n * @param element 目标元素\n * @param options 渲染选项\n */\nexport const applyContainerAttrs = (element: Element, options: MtRenderOptions): void => {\n for (const [name, value] of resolveContainerAttrs(options)) {\n element.setAttribute(name, value)\n }\n}\n\n/**\n * 用容器包裹内容 HTML。\n *\n * `renderToString`(字符串出口)与 `renderToDOM`(DOM 出口)共用本函数,\n * 因此两者返回的 `html` **必然逐字符相同**——这是「SSR 产物与客户端产物一致」的保证。\n *\n * @param output 容器内部的 HTML\n * @param options 渲染选项\n * @returns 含容器的完整 HTML\n *\n * @example\n * ```ts\n * wrapContainer('<p>hi</p>', {}) // '<div class=\"mt-content\"><p>hi</p></div>'\n * ```\n */\nexport const wrapContainer = (output: string, options: MtRenderOptions): string =>\n `<div${containerAttrsToString(options)}>${output}</div>`\n","/**\n * 内容序列化的**唯一入口**(`docs/decisions/ADR-0021`)。\n *\n * 本模块是 `renderToString` 与 `renderToDOM` 的共同上游:两个公开函数\n * 都从这里拿内容,保证「字符串出口」与「DOM 出口」不可能产出不同的节点结构。\n *\n * ⚠️ 本模块**不得做任何改写**:不加 `loading=\"lazy\"`、不注入额外的 `data-mt-*`、\n * 不做降级高亮。任何改写都会让 Web 只读渲染与小程序端 / 编辑器的节点结构\n * 不再逐字符一致(ADR-0021 决策 4)。\n */\n\nimport { serializeDocumentToHtml } from '@mteditor/document'\nimport type { MtDegradationEntry, MtDocument, MtSerializeStats } from '@mteditor/document'\n\nimport type { MtRenderOptions } from './options'\n\n/** 内容序列化结果(不含外层容器) */\nexport interface MtRenderedContent {\n /** 容器内部的 HTML(即 `document` 序列化器的原样产物) */\n output: string\n /** 降级报告;`report: false` 时为空数组 */\n report: MtDegradationEntry[]\n /** 序列化统计 */\n stats: MtSerializeStats\n}\n\n/**\n * 把 `MtDocument` 序列化为容器内部的 HTML。\n *\n * @param doc 文档\n * @param options 渲染选项\n * @returns 内容与报告、统计\n */\nexport const renderContent = (doc: MtDocument, options: MtRenderOptions): MtRenderedContent => {\n const serialized = serializeDocumentToHtml(doc, {\n preRendered: options.preRendered,\n indent: options.indent,\n })\n\n return {\n output: serialized.output,\n // §12.2:只有调用方**显式**要求关闭时才丢弃报告\n report: options.report === false ? [] : serialized.report,\n stats: serialized.stats,\n }\n}\n","/**\n * `renderToString` —— 输出 HTML 字符串,**全程不触碰 DOM**(SSR / 邮件 / 静态页)。\n */\n\nimport type { MtDocument } from '@mteditor/document'\n\nimport { wrapContainer } from './container'\nimport type { MtRenderOptions, MtRenderResult } from './options'\nimport { renderContent } from './render-content'\n\n/**\n * 把 `MtDocument` 渲染为 HTML 字符串。\n *\n * 产物 = `<div class=\"mt-content\">{document 序列化产物}</div>`,\n * 内容部分与 `@mteditor/document` 的 `serializeDocumentToHtml` **逐字符一致**\n * (ADR-0021),因此不存在「详情页与编辑器渲染不一致」的可能。\n *\n * 本函数不使用 `window` / `document` / `innerHTML`,可在 Node、边缘函数、\n * 小程序服务端等任何无 DOM 环境执行。\n *\n * @param doc 文档\n * @param options 渲染选项\n * @returns 渲染结果(`html` / `report` / `stats`)\n *\n * @example\n * ```ts\n * const { html, report } = renderToString(doc)\n * // 需要局部固定暗色时:\n * renderToString(doc, { theme: 'dark', className: 'mt-content article-body' }).html\n * // 只要字符串:\n * const str = renderToString(doc).html\n * ```\n */\nexport const renderToString = (doc: MtDocument, options: MtRenderOptions = {}): MtRenderResult => {\n const content = renderContent(doc, options)\n return {\n html: wrapContainer(content.output, options),\n report: content.report,\n stats: content.stats,\n }\n}\n","/**\n * `renderToDOM` —— 把 `MtDocument` 渲染为真实 DOM 节点并挂载进容器。\n *\n * 与 `renderToString` 的关系:**同一份内容、同一套容器属性**,\n * 因此 `renderToDOM(doc, el).html === renderToString(doc).html` 恒成立(ADR-0021)。\n */\n\nimport type { MtDocument } from '@mteditor/document'\n\nimport { applyContainerAttrs, wrapContainer } from './container'\nimport type { MtDomRenderResult, MtRenderOptions } from './options'\nimport { renderContent } from './render-content'\n\n/**\n * 把 `MtDocument` 渲染为 DOM 并挂载到 `container`。\n *\n * **挂载语义**:调用后 `container` 的子节点被**整体替换**为渲染出的容器元素\n * (即重复调用是幂等的,不会累积)。`container` 自身的属性不被修改——\n * 主题由宿主控制(AGENTS.md §7.4),需要局部覆盖时请用 `options.theme`。\n *\n * @param doc 文档\n * @param container 挂载容器(普通文档流中的元素,不得位于 `position: fixed` 容器内,§10.3)\n * @param options 渲染选项\n * @returns 渲染结果 + 实际挂载的元素\n *\n * @example\n * ```ts\n * const { element, report } = renderToDOM(doc, document.getElementById('view')!)\n * element.dataset.rendered // 可通过返回的元素继续做懒加载等增强\n * ```\n */\nexport const renderToDOM = (\n doc: MtDocument,\n container: HTMLElement,\n options: MtRenderOptions = {},\n): MtDomRenderResult => {\n const content = renderContent(doc, options)\n const element = container.ownerDocument.createElement('div')\n\n applyContainerAttrs(element, options)\n // 安全性:content.output 由 @mteditor/document 的序列化器产出——文本与属性值\n // 均已转义,仅白名单内的标签/属性可存活(docs/document-model.md §7.3,\n // 10 份 XSS 载荷基线为证)。此处不接受任何未经清洗的外部输入。\n element.innerHTML = content.output\n\n container.replaceChildren(element)\n\n return {\n html: wrapContainer(content.output, options),\n report: content.report,\n stats: content.stats,\n element,\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/options.ts","../src/container.ts","../src/render-content.ts","../src/render-to-string.ts","../src/render-to-dom.ts"],"names":[],"mappings":";;;AAOO,IAAM,gBAAA,GAAmB;AAGzB,IAAM,aAAA,GAAgB;ACiBtB,IAAM,qBAAA,GAAwB,CAAC,OAAA,KAAgD;AACpF,EAAA,MAAM,QAA2B,EAAC;AAClC,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,gBAAA;AACvC,EAAA,IAAI,SAAA,CAAU,SAAS,CAAA,EAAG,KAAA,CAAM,KAAK,CAAC,OAAA,EAAS,SAAS,CAAC,CAAA;AACzD,EAAA,IAAI,OAAA,CAAQ,UAAU,MAAA,EAAW,KAAA,CAAM,KAAK,CAAC,aAAA,EAAe,OAAA,CAAQ,KAAK,CAAC,CAAA;AAC1E,EAAA,OAAO,KAAA;AACT;AAaO,IAAM,sBAAA,GAAyB,CAAC,OAAA,KACrC,qBAAA,CAAsB,OAAO,CAAA,CAC1B,GAAA,CAAI,CAAC,CAAC,IAAA,EAAM,KAAK,CAAA,KAAM,CAAA,CAAA,EAAI,IAAI,CAAA,EAAA,EAAK,UAAA,CAAW,KAAK,CAAC,CAAA,CAAA,CAAG,CAAA,CACxD,IAAA,CAAK,EAAE;AAQL,IAAM,mBAAA,GAAsB,CAAC,OAAA,EAAkB,OAAA,KAAmC;AACvF,EAAA,KAAA,MAAW,CAAC,IAAA,EAAM,KAAK,CAAA,IAAK,qBAAA,CAAsB,OAAO,CAAA,EAAG;AAC1D,IAAA,OAAA,CAAQ,YAAA,CAAa,MAAM,KAAK,CAAA;AAAA,EAClC;AACF;AAiBO,IAAM,aAAA,GAAgB,CAAC,MAAA,EAAgB,OAAA,KAC5C,OAAO,sBAAA,CAAuB,OAAO,CAAC,CAAA,CAAA,EAAI,MAAM,CAAA,MAAA;AC9C3C,IAAM,aAAA,GAAgB,CAAC,GAAA,EAAiB,OAAA,KAAgD;AAC7F,EAAA,MAAM,UAAA,GAAa,wBAAwB,GAAA,EAAK;AAAA,IAC9C,aAAa,OAAA,CAAQ,WAAA;AAAA,IACrB,QAAQ,OAAA,CAAQ;AAAA,GACjB,CAAA;AAED,EAAA,OAAO;AAAA,IACL,QAAQ,UAAA,CAAW,MAAA;AAAA;AAAA,IAEnB,QAAQ,OAAA,CAAQ,MAAA,KAAW,KAAA,GAAQ,KAAK,UAAA,CAAW,MAAA;AAAA,IACnD,OAAO,UAAA,CAAW;AAAA,GACpB;AACF,CAAA;;;ACZO,IAAM,cAAA,GAAiB,CAAC,GAAA,EAAiB,OAAA,GAA2B,EAAC,KAAsB;AAChG,EAAA,MAAM,OAAA,GAAU,aAAA,CAAc,GAAA,EAAK,OAAO,CAAA;AAC1C,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,aAAA,CAAc,OAAA,CAAQ,MAAA,EAAQ,OAAO,CAAA;AAAA,IAC3C,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,OAAO,OAAA,CAAQ;AAAA,GACjB;AACF;;;ACTO,IAAM,cAAc,CACzB,GAAA,EACA,SAAA,EACA,OAAA,GAA2B,EAAC,KACN;AACtB,EAAA,MAAM,OAAA,GAAU,aAAA,CAAc,GAAA,EAAK,OAAO,CAAA;AAC1C,EAAA,MAAM,OAAA,GAAU,SAAA,CAAU,aAAA,CAAc,aAAA,CAAc,KAAK,CAAA;AAE3D,EAAA,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAIpC,EAAA,OAAA,CAAQ,YAAY,OAAA,CAAQ,MAAA;AAE5B,EAAA,SAAA,CAAU,gBAAgB,OAAO,CAAA;AAEjC,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,aAAA,CAAc,OAAA,CAAQ,MAAA,EAAQ,OAAO,CAAA;AAAA,IAC3C,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,OAAO,OAAA,CAAQ,KAAA;AAAA,IACf;AAAA,GACF;AACF","file":"index.js","sourcesContent":["/**\n * `@mteditor/renderer-web` 的公开选项与结果类型。\n */\n\nimport type { MtDegradationEntry, MtPreRenderedIndex, MtSerializeStats } from '@mteditor/document'\n\n/** 内容容器类名 —— 与 `@mteditor/theme-default` 的 `.mt-content` 约定一致 */\nexport const MT_CONTENT_CLASS = 'mt-content'\n\n/** 主题属性名 —— 主题层只认这一个属性 */\nexport const MT_THEME_ATTR = 'data-mt-theme'\n\n/** 主题取值;不开放任意字符串,避免写出主题层无法识别的属性值 */\nexport type MtTheme = 'light' | 'dark'\n\n/** 渲染选项;全部字段可选 */\nexport interface MtRenderOptions {\n /**\n * 容器的**完整** `class` 值,缺省 `'mt-content'`。\n *\n * 需要追加自己的类名时请一并写出,例如 `'mt-content article-body'`;\n * 传空字符串则完全不输出 `class` 属性(此时将失去主题排版样式)。\n */\n className?: string\n /**\n * 主题。**缺省不写 `data-mt-theme`**,跟随宿主(主题模型)。\n *\n * 只在需要**局部覆盖**时显式传入,例如邮件预览固定亮色。\n */\n theme?: MtTheme\n /** 预渲染产物索引(公式 SVG / 代码高亮) */\n preRendered?: MtPreRenderedIndex\n /** 缩进层级,缺省 `0`;仅影响 HTML 可读性,不参与语义 */\n indent?: number\n /** 是否返回降级报告,缺省 `true`。**置 `false` 等于主动放弃降级信息** */\n report?: boolean\n}\n\n/** 渲染结果(`renderToString` 的返回值) */\nexport interface MtRenderResult {\n /** 完整 HTML,**含外层容器** */\n html: string\n /** 降级报告;`report: false` 时为空数组 */\n report: MtDegradationEntry[]\n /** 序列化统计(**不含**外层容器,即 `document` 序列化器的原始统计) */\n stats: MtSerializeStats\n}\n\n/** DOM 渲染结果(`renderToDOM` 的返回值) */\nexport interface MtDomRenderResult extends MtRenderResult {\n /** 实际挂载进容器的元素 */\n element: HTMLElement\n}\n","/**\n * 渲染容器的属性装配。\n *\n * 容器属性有**两条出口**:字符串出口(`renderToString`)与真实元素出口(`renderToDOM`)。\n * 两者必须产出同一组属性,因此都从本模块的 `resolveContainerAttrs` 取值——\n * 这是「SSR 字符串与客户端 DOM 一致」的实现基础。\n */\n\nimport { escapeAttr } from '@mteditor/document'\n\nimport { MT_CONTENT_CLASS, MT_THEME_ATTR, type MtRenderOptions } from './options'\n\n/** 属性对:`[属性名, 属性值]`,值必为字符串(无「缺省」概念,缺省即不产出该对) */\ntype MtContainerAttr = readonly [name: string, value: string]\n\n/**\n * 解析容器应当产出的属性对(顺序固定:`class` 在前)。\n *\n * @param options 渲染选项\n * @returns 属性对列表;可能为空数组\n *\n * @example\n * ```ts\n * resolveContainerAttrs({ className: '' }) // []\n * resolveContainerAttrs({ theme: 'dark' }) // [['class', 'mt-content'], ['data-mt-theme', 'dark']]\n * ```\n */\nexport const resolveContainerAttrs = (options: MtRenderOptions): MtContainerAttr[] => {\n const attrs: MtContainerAttr[] = []\n const className = options.className ?? MT_CONTENT_CLASS\n if (className.length > 0) attrs.push(['class', className])\n if (options.theme !== undefined) attrs.push([MT_THEME_ATTR, options.theme])\n return attrs\n}\n\n/**\n * 把容器属性序列化为 HTML 属性串。\n *\n * @param options 渲染选项\n * @returns 形如 ` class=\"mt-content\"` 的字符串;无属性时为空串\n *\n * @example\n * ```ts\n * containerAttrsToString({}) // ' class=\"mt-content\"'\n * ```\n */\nexport const containerAttrsToString = (options: MtRenderOptions): string =>\n resolveContainerAttrs(options)\n .map(([name, value]) => ` ${name}=\"${escapeAttr(value)}\"`)\n .join('')\n\n/**\n * 把容器属性写到真实元素上。\n *\n * @param element 目标元素\n * @param options 渲染选项\n */\nexport const applyContainerAttrs = (element: Element, options: MtRenderOptions): void => {\n for (const [name, value] of resolveContainerAttrs(options)) {\n element.setAttribute(name, value)\n }\n}\n\n/**\n * 用容器包裹内容 HTML。\n *\n * `renderToString`(字符串出口)与 `renderToDOM`(DOM 出口)共用本函数,\n * 因此两者返回的 `html` **必然逐字符相同**——这是「SSR 产物与客户端产物一致」的保证。\n *\n * @param output 容器内部的 HTML\n * @param options 渲染选项\n * @returns 含容器的完整 HTML\n *\n * @example\n * ```ts\n * wrapContainer('<p>hi</p>', {}) // '<div class=\"mt-content\"><p>hi</p></div>'\n * ```\n */\nexport const wrapContainer = (output: string, options: MtRenderOptions): string =>\n `<div${containerAttrsToString(options)}>${output}</div>`\n","/**\n * 内容序列化的**唯一入口**。\n *\n * 本模块是 `renderToString` 与 `renderToDOM` 的共同上游:两个公开函数\n * 都从这里拿内容,保证「字符串出口」与「DOM 出口」不可能产出不同的节点结构。\n *\n * ⚠️ 本模块**不得做任何改写**:不加 `loading=\"lazy\"`、不注入额外的 `data-mt-*`、\n * 不做降级高亮。任何改写都会让 Web 只读渲染与小程序端 / 编辑器的节点结构\n * 不再逐字符一致。\n */\n\nimport { serializeDocumentToHtml } from '@mteditor/document'\nimport type { MtDegradationEntry, MtDocument, MtSerializeStats } from '@mteditor/document'\n\nimport type { MtRenderOptions } from './options'\n\n/** 内容序列化结果(不含外层容器) */\nexport interface MtRenderedContent {\n /** 容器内部的 HTML(即 `document` 序列化器的原样产物) */\n output: string\n /** 降级报告;`report: false` 时为空数组 */\n report: MtDegradationEntry[]\n /** 序列化统计 */\n stats: MtSerializeStats\n}\n\n/**\n * 把 `MtDocument` 序列化为容器内部的 HTML。\n *\n * @param doc 文档\n * @param options 渲染选项\n * @returns 内容与报告、统计\n */\nexport const renderContent = (doc: MtDocument, options: MtRenderOptions): MtRenderedContent => {\n const serialized = serializeDocumentToHtml(doc, {\n preRendered: options.preRendered,\n indent: options.indent,\n })\n\n return {\n output: serialized.output,\n // 只有调用方**显式**要求关闭时才丢弃报告\n report: options.report === false ? [] : serialized.report,\n stats: serialized.stats,\n }\n}\n","/**\n * `renderToString` —— 输出 HTML 字符串,**全程不触碰 DOM**(SSR / 邮件 / 静态页)。\n */\n\nimport type { MtDocument } from '@mteditor/document'\n\nimport { wrapContainer } from './container'\nimport type { MtRenderOptions, MtRenderResult } from './options'\nimport { renderContent } from './render-content'\n\n/**\n * 把 `MtDocument` 渲染为 HTML 字符串。\n *\n * 产物 = `<div class=\"mt-content\">{document 序列化产物}</div>`,\n * 内容部分与 `@mteditor/document` 的 `serializeDocumentToHtml` **逐字符一致**,\n * 因此不存在「详情页与编辑器渲染不一致」的可能。\n *\n * 本函数不使用 `window` / `document` / `innerHTML`,可在 Node、边缘函数、\n * 小程序服务端等任何无 DOM 环境执行。\n *\n * @param doc 文档\n * @param options 渲染选项\n * @returns 渲染结果(`html` / `report` / `stats`)\n *\n * @example\n * ```ts\n * const { html, report } = renderToString(doc)\n * // 需要局部固定暗色时:\n * renderToString(doc, { theme: 'dark', className: 'mt-content article-body' }).html\n * // 只要字符串:\n * const str = renderToString(doc).html\n * ```\n */\nexport const renderToString = (doc: MtDocument, options: MtRenderOptions = {}): MtRenderResult => {\n const content = renderContent(doc, options)\n return {\n html: wrapContainer(content.output, options),\n report: content.report,\n stats: content.stats,\n }\n}\n","/**\n * `renderToDOM` —— 把 `MtDocument` 渲染为真实 DOM 节点并挂载进容器。\n *\n * 与 `renderToString` 的关系:**同一份内容、同一套容器属性**,\n * 因此 `renderToDOM(doc, el).html === renderToString(doc).html` 恒成立。\n */\n\nimport type { MtDocument } from '@mteditor/document'\n\nimport { applyContainerAttrs, wrapContainer } from './container'\nimport type { MtDomRenderResult, MtRenderOptions } from './options'\nimport { renderContent } from './render-content'\n\n/**\n * 把 `MtDocument` 渲染为 DOM 并挂载到 `container`。\n *\n * **挂载语义**:调用后 `container` 的子节点被**整体替换**为渲染出的容器元素\n * (即重复调用是幂等的,不会累积)。`container` 自身的属性不被修改——\n * 主题由宿主控制,需要局部覆盖时请用 `options.theme`。\n *\n * @param doc 文档\n * @param container 挂载容器(普通文档流中的元素,不得位于 `position: fixed` 容器内)\n * @param options 渲染选项\n * @returns 渲染结果 + 实际挂载的元素\n *\n * @example\n * ```ts\n * const { element, report } = renderToDOM(doc, document.getElementById('view')!)\n * element.dataset.rendered // 可通过返回的元素继续做懒加载等增强\n * ```\n */\nexport const renderToDOM = (\n doc: MtDocument,\n container: HTMLElement,\n options: MtRenderOptions = {},\n): MtDomRenderResult => {\n const content = renderContent(doc, options)\n const element = container.ownerDocument.createElement('div')\n\n applyContainerAttrs(element, options)\n // 安全性:content.output 由 @mteditor/document 的序列化器产出——文本与属性值\n // 均已转义,仅白名单内的标签/属性可存活(10 份 XSS 载荷基线为证)。\n // 此处不接受任何未经清洗的外部输入。\n element.innerHTML = content.output\n\n container.replaceChildren(element)\n\n return {\n html: wrapContainer(content.output, options),\n report: content.report,\n stats: content.stats,\n element,\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mteditor/renderer-web",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Web 端只读渲染器:把 MtDocument 渲染为 HTML,不依赖编辑器运行时",
5
5
  "keywords": [
6
6
  "mteditor",
@@ -50,7 +50,7 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@mteditor/document": "0.1.0"
53
+ "@mteditor/document": "0.2.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "happy-dom": "^15.11.7",