@maxoyed/ode-core 0.0.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/extensions/index.ts","../src/extensions/official-role.ts","../src/extensions/official-image.ts","../src/extensions/hr-variant.ts","../src/extensions/pagination.ts","../src/templates.ts","../src/editor.ts","../src/styles/inject.ts","../src/fonts/register.ts","../src/preview/paginated.ts"],"sourcesContent":["/**\n * 公文编辑器扩展集合:StarterKit + 公文要素角色 + 文本样式/对齐/颜色。\n */\nimport StarterKit from \"@tiptap/starter-kit\";\nimport TextStyle from \"@tiptap/extension-text-style\";\nimport TextAlign from \"@tiptap/extension-text-align\";\nimport Color from \"@tiptap/extension-color\";\nimport Table from \"@tiptap/extension-table\";\nimport TableRow from \"@tiptap/extension-table-row\";\nimport TableCell from \"@tiptap/extension-table-cell\";\nimport TableHeader from \"@tiptap/extension-table-header\";\nimport type { Extensions } from \"@tiptap/core\";\nimport { OfficialRole } from \"./official-role\";\nimport { OfficialImage } from \"./official-image\";\nimport { HorizontalRuleVariant } from \"./hr-variant\";\nimport { Pagination } from \"./pagination\";\n\nexport interface OfficialExtensionsOptions {\n /** 占位提示文字 */\n placeholder?: string;\n /** 是否启用编辑器内联实时分页(默认关闭,需浏览器环境) */\n pagination?: boolean;\n}\n\nexport function getOfficialExtensions(options: OfficialExtensionsOptions = {}): Extensions {\n const extensions: Extensions = [\n StarterKit.configure({\n // 公文标题层级用 officialRole 表达,禁用通用 heading 以免样式冲突\n heading: false,\n }),\n TextStyle,\n Color,\n TextAlign.configure({ types: [\"paragraph\"] }),\n OfficialRole.configure({ types: [\"paragraph\"] }),\n HorizontalRuleVariant,\n Table.configure({ resizable: false }),\n TableRow,\n TableHeader,\n TableCell,\n OfficialImage.configure({ allowBase64: true, inline: false }),\n ];\n if (options.pagination) extensions.push(Pagination);\n return extensions;\n}\n\nexport { OfficialRole, Pagination, HorizontalRuleVariant, OfficialImage };\nexport type { PaginationOptions } from \"./pagination\";\nexport type { HrVariant } from \"./hr-variant\";\n","/**\n * OfficialRole 扩展:为块级节点附加“公文要素角色”属性。\n *\n * 不直接写死样式,而是输出 data-odoc-role 与 class,由公文样式表(按 ELEMENT_SPEC\n * 生成)统一渲染字体/字号/对齐/缩进,保证渲染与 docx 导出共享同一份规范。\n */\nimport { Extension } from \"@tiptap/core\";\nimport type { OfficialElement } from \"../spec/elements\";\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n officialRole: {\n /** 将当前段落设为指定公文要素(如 title、headingLevel1、dateline)。 */\n setOfficialRole: (role: OfficialElement) => ReturnType;\n /** 清除当前段落的公文要素角色(回退为普通正文)。 */\n unsetOfficialRole: () => ReturnType;\n };\n }\n}\n\nexport interface OfficialRoleOptions {\n /** 应用角色属性的节点类型,默认作用于 paragraph。 */\n types: string[];\n}\n\nexport const OfficialRole = Extension.create<OfficialRoleOptions>({\n name: \"officialRole\",\n\n addOptions() {\n return { types: [\"paragraph\"] };\n },\n\n addGlobalAttributes() {\n return [\n {\n types: this.options.types,\n attributes: {\n officialRole: {\n default: null,\n parseHTML: (element) => element.getAttribute(\"data-odoc-role\"),\n renderHTML: (attributes) => {\n const role = attributes.officialRole as OfficialElement | null;\n if (!role) return {};\n return {\n \"data-odoc-role\": role,\n class: `odoc-el odoc-el--${role}`,\n };\n },\n },\n },\n },\n ];\n },\n\n addCommands() {\n return {\n setOfficialRole:\n (role) =>\n ({ commands }) =>\n this.options.types.some((type) =>\n commands.updateAttributes(type, { officialRole: role }),\n ),\n unsetOfficialRole:\n () =>\n ({ commands }) =>\n this.options.types.some((type) =>\n commands.resetAttributes(type, \"officialRole\"),\n ),\n };\n },\n});\n","/**\n * 在 Tiptap Image 基础上扩展公文图片属性:\n * - seal:是否为印章(红色印章,排版时叠加于成文日期之上)\n *\n * 印章在编辑器内以叠加样式呈现(.odoc-seal);docx 导出为浮动图片(允许叠压),\n * 导入时若图片为浮动锚定(wp:anchor)则还原为印章。\n */\nimport Image from \"@tiptap/extension-image\";\n\nexport const OfficialImage = Image.extend({\n name: \"image\",\n addAttributes() {\n return {\n ...this.parent?.(),\n seal: {\n default: false,\n parseHTML: (element) => element.hasAttribute(\"data-odoc-seal\"),\n renderHTML: (attributes) =>\n attributes.seal ? { \"data-odoc-seal\": \"true\", class: \"odoc-seal\" } : {},\n },\n };\n },\n});\n","/**\n * 为分隔线(horizontalRule)增加公文变体属性:\n * - reverse:红头反线(发文字号下方红色分隔线,默认)\n * - record:版记分隔线(版记区上下黑色全幅线)\n *\n * 仅附加 data 属性与 class,颜色由样式表渲染;docx 导出/导入据此区分边框颜色。\n */\nimport { Extension } from \"@tiptap/core\";\n\nexport type HrVariant = \"reverse\" | \"record\";\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n hrVariant: {\n /** 插入指定变体的分隔线 */\n setHorizontalRuleVariant: (variant: HrVariant) => ReturnType;\n };\n }\n}\n\nexport interface HrVariantOptions {\n types: string[];\n}\n\nexport const HorizontalRuleVariant = Extension.create<HrVariantOptions>({\n name: \"hrVariant\",\n\n addOptions() {\n return { types: [\"horizontalRule\"] };\n },\n\n addGlobalAttributes() {\n return [\n {\n types: this.options.types,\n attributes: {\n variant: {\n default: \"reverse\",\n parseHTML: (element) => element.getAttribute(\"data-odoc-hr\") || \"reverse\",\n renderHTML: (attributes) => {\n const variant = (attributes.variant as HrVariant) || \"reverse\";\n return { \"data-odoc-hr\": variant, class: `odoc-hr odoc-hr--${variant}` };\n },\n },\n },\n },\n ];\n },\n\n addCommands() {\n return {\n setHorizontalRuleVariant:\n (variant) =>\n ({ chain }) =>\n chain()\n .insertContent({ type: \"horizontalRule\", attrs: { variant } })\n .run(),\n };\n },\n});\n","/**\n * 编辑器内联实时分页(Tiptap/ProseMirror 扩展)。\n *\n * 测量可编辑区内各顶层块的真实位置,借助 headless computePageBreaks 计算断点,\n * 以 widget 装饰插入“分页间隔”,把单一 contenteditable 可视化为多张 A4 页面,\n * 并在每页版心下方编排页码(单页居右、双页居左空一字)。\n *\n * 说明:本扩展依赖浏览器布局,需在真实浏览器中目视核对;为可选扩展,\n * 不影响默认编辑器。断页几何(computePageBreaks)已单测覆盖。\n * v1 以块为断页粒度,超长段落跨页断行留待后续。\n */\nimport { Extension } from \"@tiptap/core\";\nimport { Plugin, PluginKey } from \"@tiptap/pm/state\";\nimport { Decoration, DecorationSet } from \"@tiptap/pm/view\";\nimport type { EditorView } from \"@tiptap/pm/view\";\nimport { TYPE_AREA_MM, MARGIN_MM } from \"../spec/layout\";\nimport { computePageBreaks, type BlockRect } from \"../pagination/layout\";\nimport { pageNumberStyle } from \"../pagination/page-number\";\n\nconst MM_TO_PX = 96 / 25.4;\n\nexport interface PaginationOptions {\n /** 每页版心内容高度(px),默认 225mm */\n pageContentPx: number;\n /** 页间视觉间隔(px) */\n pageGapPx: number;\n /** 天头(上白边)px,用于页间留白 */\n topMarginPx: number;\n /** 地脚(下白边)px,用于页间留白 */\n bottomMarginPx: number;\n /** 是否渲染页码 */\n showPageNumbers: boolean;\n}\n\nconst pluginKey = new PluginKey<DecorationSet>(\"odocPagination\");\n\ninterface BreakMeta {\n decorations: DecorationSet;\n}\n\nfunction buildPageNumberEl(pageNo: number): HTMLElement {\n const style = pageNumberStyle(pageNo);\n const el = document.createElement(\"div\");\n el.className = \"odoc-inline-page-number\";\n el.textContent = style.text;\n el.style.position = \"absolute\";\n // spacer 为全幅(含订口/切口),故页码自纸张边缘内缩 切口/订口 + 空一字\n if (style.align === \"right\") {\n el.style.right = `${(MARGIN_MM.right + style.insetMm) * MM_TO_PX}px`;\n } else {\n el.style.left = `${(MARGIN_MM.left + style.insetMm) * MM_TO_PX}px`;\n }\n return el;\n}\n\ninterface SpacerMetrics {\n spacerPx: number;\n /** 上一页版心剩余空白(白),= spacerPx - breakExtraPx */\n remainingPx: number;\n gapPx: number;\n topMarginPx: number;\n bottomMarginPx: number;\n /** 版心下边缘距页码 7mm 的像素值 */\n pageNumberOffsetPx: number;\n endingPageNo: number;\n showPageNumbers: boolean;\n}\n\nfunction buildSpacer(m: SpacerMetrics): HTMLElement {\n const spacer = document.createElement(\"div\");\n spacer.className = \"odoc-page-break\";\n spacer.style.display = \"block\"; // 置于段落内部时强制断行,把后续行推至下一页\n spacer.style.height = `${m.spacerPx}px`;\n spacer.setAttribute(\"contenteditable\", \"false\");\n\n // 纵向构成:白(上一页剩余版心 + 地脚) → 灰(页间空隙) → 白(下一页天头)\n const whiteTop = m.remainingPx + m.bottomMarginPx;\n const grey = m.gapPx;\n spacer.style.background = `linear-gradient(to bottom, var(--odoc-page-bg) 0 ${whiteTop}px, var(--odoc-canvas-bg) ${whiteTop}px ${whiteTop + grey}px, var(--odoc-page-bg) ${whiteTop + grey}px 100%)`;\n\n if (m.showPageNumbers) {\n const num = buildPageNumberEl(m.endingPageNo);\n // 页码位于上一页版心下边缘之下 7mm\n num.style.top = `${m.remainingPx + m.pageNumberOffsetPx}px`;\n num.style.bottom = \"\";\n spacer.appendChild(num);\n }\n return spacer;\n}\n\n/**\n * 行级测量:取每个顶层块的逐行行盒(Range.getClientRects),换算为“自然位置”\n * (减去其上方所有 spacer 高度,兼容嵌在段落内部的内联 spacer),并用 posAtCoords\n * 求出每行起点的文档位置。据此即可在超长段落中间按行断页。\n *\n * 以行为单位喂给 computePageBreaks:断点落在某行起点时,若该行位于段落中部,\n * 插入的分隔 widget 即把后续行推至下一页,实现段落跨页断行。\n */\nfunction measureLines(view: EditorView): { rects: BlockRect[]; positions: number[] } {\n const rootRect = view.dom.getBoundingClientRect();\n\n // 所有已插入的 spacer(含嵌套在段落内的内联 spacer)\n const spacers = Array.from(view.dom.querySelectorAll<HTMLElement>(\".odoc-page-break\")).map(\n (s) => {\n const r = s.getBoundingClientRect();\n return { top: r.top, h: r.height };\n },\n );\n const offsetAbove = (yTop: number) =>\n spacers.reduce((acc, s) => (s.top < yTop - 0.5 ? acc + s.h : acc), 0);\n\n const rects: BlockRect[] = [];\n const positions: number[] = [];\n let lastPos = -1;\n\n const children = view.dom.children;\n for (let i = 0; i < children.length; i++) {\n const el = children[i] as HTMLElement;\n if (el.classList.contains(\"odoc-page-break\")) continue;\n\n // 取该块的行盒;空块/原子块退化为整块一行\n let lineRects: DOMRect[] = [];\n if (el.firstChild) {\n const range = document.createRange();\n range.selectNodeContents(el);\n lineRects = Array.from(range.getClientRects());\n }\n if (lineRects.length === 0) lineRects = [el.getBoundingClientRect()];\n\n for (const lr of lineRects) {\n if (lr.height < 1) continue;\n const at = view.posAtCoords({ left: lr.left + 1, top: lr.top + lr.height / 2 });\n if (!at) continue;\n if (at.pos === lastPos) continue; // 同一行的重复行盒去重\n lastPos = at.pos;\n rects.push({ top: lr.top - rootRect.top - offsetAbove(lr.top), height: lr.height });\n positions.push(at.pos);\n }\n }\n return { rects, positions };\n}\n\nfunction signatureOf(decoset: { beforeIndex: number; spacerPx: number }[]): string {\n return decoset.map((b) => `${b.beforeIndex}:${Math.round(b.spacerPx)}`).join(\"|\");\n}\n\nexport const Pagination = Extension.create<PaginationOptions>({\n name: \"odocPagination\",\n\n addOptions() {\n return {\n pageContentPx: TYPE_AREA_MM.height * MM_TO_PX,\n pageGapPx: 24,\n topMarginPx: MARGIN_MM.top * MM_TO_PX,\n bottomMarginPx: MARGIN_MM.bottom * MM_TO_PX,\n showPageNumbers: true,\n };\n },\n\n addProseMirrorPlugins() {\n const options = this.options;\n let lastSignature = \"\";\n let raf = 0;\n\n return [\n new Plugin<DecorationSet>({\n key: pluginKey,\n state: {\n init: () => DecorationSet.empty,\n apply(tr, old) {\n const meta = tr.getMeta(pluginKey) as BreakMeta | undefined;\n if (meta) return meta.decorations;\n return old.map(tr.mapping, tr.doc);\n },\n },\n props: {\n decorations(state) {\n return pluginKey.getState(state);\n },\n },\n view(view) {\n const breakExtraPx = options.bottomMarginPx + options.pageGapPx + options.topMarginPx;\n const pageNumberOffsetPx = 7 * MM_TO_PX; // 版心下边缘距页码 7mm\n\n const recompute = () => {\n const { rects, positions } = measureLines(view);\n const { breaks, pageCount } = computePageBreaks(rects, {\n pageContentPx: options.pageContentPx,\n breakExtraPx,\n });\n\n // 末页:补足整张 A4 白纸并编排末页页码\n let tailRemaining = 0;\n if (rects.length) {\n const last = rects[rects.length - 1];\n const lastPageStart = breaks.length\n ? rects[breaks[breaks.length - 1].beforeIndex].top\n : rects[0].top;\n tailRemaining = Math.max(\n 0,\n options.pageContentPx - (last.top + last.height - lastPageStart),\n );\n }\n\n const sig = `${signatureOf(breaks)}#${pageCount}:${Math.round(tailRemaining)}`;\n if (sig === lastSignature) return;\n lastSignature = sig;\n\n const decos = breaks.map((b) =>\n Decoration.widget(\n positions[b.beforeIndex],\n () =>\n buildSpacer({\n spacerPx: b.spacerPx,\n remainingPx: Math.max(0, b.spacerPx - breakExtraPx),\n gapPx: options.pageGapPx,\n topMarginPx: options.topMarginPx,\n bottomMarginPx: options.bottomMarginPx,\n pageNumberOffsetPx,\n endingPageNo: b.pageNo - 1,\n showPageNumbers: options.showPageNumbers,\n }),\n { side: -1, key: `odoc-break-${b.beforeIndex}` },\n ),\n );\n\n if (rects.length && options.showPageNumbers) {\n // 末页补白:填满版心剩余 + 地脚(无页间空隙、无下一页天头),页码落于地脚\n decos.push(\n Decoration.widget(\n view.state.doc.content.size,\n () =>\n buildSpacer({\n spacerPx: tailRemaining + options.bottomMarginPx,\n remainingPx: tailRemaining,\n gapPx: 0,\n topMarginPx: 0,\n bottomMarginPx: options.bottomMarginPx,\n pageNumberOffsetPx,\n endingPageNo: pageCount,\n showPageNumbers: true,\n }),\n { side: 1, key: `odoc-tail-${pageCount}` },\n ),\n );\n }\n const decorations = DecorationSet.create(view.state.doc, decos);\n const tr = view.state.tr.setMeta(pluginKey, { decorations } satisfies BreakMeta);\n view.dispatch(tr);\n };\n\n const schedule = () => {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(recompute);\n };\n\n schedule();\n return {\n update: schedule,\n destroy: () => cancelAnimationFrame(raf),\n };\n },\n }),\n ];\n },\n});\n","/**\n * 公文模板:返回 Tiptap/ProseMirror JSON 文档内容。\n * 让编辑器“开箱即默认公文版式”,使用方可直接加载或在此基础上修改。\n */\nimport type { JSONContent } from \"@tiptap/core\";\nimport type { OfficialElement } from \"./spec/elements\";\n\nfunction p(role: OfficialElement | null, text: string): JSONContent {\n const node: JSONContent = { type: \"paragraph\" };\n if (role) node.attrs = { officialRole: role };\n if (text) node.content = [{ type: \"text\", text }];\n return node;\n}\n\n/** 标准红头文件(下行文)骨架模板。 */\nexport function redHeadDocumentTemplate(): JSONContent {\n return {\n type: \"doc\",\n content: [\n p(\"issuer\", \"○○○人民政府文件\"),\n p(\"docNumber\", \"○府〔2026〕1 号\"),\n { type: \"horizontalRule\" },\n p(\"title\", \"关于×××工作的通知\"),\n p(\"mainRecipient\", \"各有关单位:\"),\n p(\"body\", \"根据有关工作部署,现就×××工作通知如下。\"),\n p(\"headingLevel1\", \"一、总体要求\"),\n p(\"body\", \"(此处填写正文内容。)\"),\n p(\"headingLevel2\", \"(一)工作目标\"),\n p(\"body\", \"(此处填写正文内容。)\"),\n p(\"signature\", \"○○○人民政府\"),\n p(\"dateline\", \"2026 年 6 月 13 日\"),\n ],\n };\n}\n\n/** 空白公文(仅一个正文段落)。 */\nexport function blankDocumentTemplate(): JSONContent {\n return { type: \"doc\", content: [p(\"body\", \"\")] };\n}\n","/**\n * createOfficialDocumentEditor —— headless 公文编辑器工厂。\n *\n * 基于 Tiptap,与具体前端框架无关;Vue/React 适配层只需在其上做薄封装。\n * 创建时自动注入公文要素样式,并默认加载公文版式模板。\n */\nimport { Editor, type EditorOptions, type JSONContent } from \"@tiptap/core\";\nimport { getOfficialExtensions } from \"./extensions\";\nimport { injectOfficialStyles } from \"./styles/inject\";\nimport { redHeadDocumentTemplate } from \"./templates\";\n\nexport interface OfficialEditorOptions\n extends Partial<Omit<EditorOptions, \"extensions\" | \"content\">> {\n /** 挂载元素(headless 使用可不传,自行渲染) */\n element?: EditorOptions[\"element\"];\n /** 初始内容;缺省加载标准红头文件模板 */\n content?: JSONContent | string;\n /** 占位提示 */\n placeholder?: string;\n /** 是否自动注入公文要素样式(默认 true) */\n injectStyles?: boolean;\n /** 是否启用编辑器内联实时分页(默认 false,需浏览器环境) */\n pagination?: boolean;\n}\n\nexport function createOfficialDocumentEditor(\n options: OfficialEditorOptions = {},\n): Editor {\n const {\n content,\n placeholder,\n pagination = false,\n injectStyles = true,\n editorProps,\n ...rest\n } = options;\n\n if (injectStyles) injectOfficialStyles();\n\n return new Editor({\n ...rest,\n editorProps: {\n ...editorProps,\n attributes: {\n class: \"odoc-typearea\",\n ...(editorProps?.attributes as Record<string, string> | undefined),\n },\n },\n extensions: getOfficialExtensions({ placeholder, pagination }),\n content: content ?? redHeadDocumentTemplate(),\n });\n}\n","/**\n * 依据 ELEMENT_SPEC 生成公文要素样式表并注入文档。\n *\n * 这样字体/字号/对齐/缩进只有一份“事实来源”(spec/elements.ts),\n * 渲染(CSS)与 docx 导出可保持一致,避免手写 CSS 与规范漂移。\n */\nimport { ELEMENT_SPEC, type ElementSpec, type OfficialElement } from \"../spec/elements\";\nimport { FONT_CSS_VAR, FONT_STACK, type FontRole } from \"../spec/fonts\";\nimport { toPt } from \"../spec/font-size\";\n\nconst STYLE_ELEMENT_ID = \"odoc-element-styles\";\n\nfunction fontVar(role: FontRole): string {\n return `var(${FONT_CSS_VAR[role]}, ${FONT_STACK[role]})`;\n}\n\nfunction ruleFor(role: OfficialElement, spec: ElementSpec): string {\n const decls: string[] = [];\n decls.push(`font-family:${fontVar(spec.font)}`);\n decls.push(`font-size:${toPt(spec.size)}pt`);\n decls.push(`font-weight:${spec.bold ? 700 : 400}`);\n if (spec.align) decls.push(`text-align:${spec.align}`);\n if (spec.color) decls.push(`color:${spec.color}`);\n // 以“字”为单位的缩进用 em(1 中文字 ≈ 1em)\n if (spec.indent) decls.push(`text-indent:${spec.indent}em`);\n if (spec.marginLeft) decls.push(`margin-left:${spec.marginLeft}em`);\n if (spec.marginRight) decls.push(`margin-right:${spec.marginRight}em`);\n return `.odoc-el--${role}{${decls.join(\";\")}}`;\n}\n\n/** 生成全部公文要素样式的 CSS 文本。 */\nexport function buildElementStyles(): string {\n return (Object.keys(ELEMENT_SPEC) as OfficialElement[])\n .map((role) => ruleFor(role, ELEMENT_SPEC[role]))\n .join(\"\\n\");\n}\n\n/** 将公文要素样式注入页面(幂等,仅注入一次)。SSR 环境下静默跳过。 */\nexport function injectOfficialStyles(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(STYLE_ELEMENT_ID)) return;\n const style = document.createElement(\"style\");\n style.id = STYLE_ELEMENT_ID;\n style.textContent = buildElementStyles();\n document.head.appendChild(style);\n}\n","/**\n * 字体插槽:允许使用方在运行时注入授权公文字体,覆盖默认开源兜底栈。\n *\n * 适用于内网 / 离线环境需要精确还原仿宋_GB2312、方正小标宋等商业字体的场景,\n * 字体文件由使用方自行提供并保证授权合规,本库不分发任何商业字体。\n */\nimport { FONT_CSS_VAR, type FontRole } from \"../spec/fonts\";\n\nexport interface RegisterFontOptions {\n /** 对应的公文字体角色 */\n role: FontRole;\n /** font-family 名称(将注入 @font-face 并设为该角色的最高优先级) */\n family: string;\n /** 字体文件来源:URL 字符串、ArrayBuffer、或 Blob */\n source?: string | ArrayBuffer | Blob;\n /** @font-face 格式提示,如 'woff2'、'truetype' */\n format?: string;\n /** 字重 */\n weight?: string | number;\n /** 作用范围根节点,默认 document.documentElement */\n target?: HTMLElement;\n}\n\nconst STYLE_ELEMENT_ID = \"odoc-registered-fonts\";\n\nfunction ensureStyleElement(): HTMLStyleElement {\n let el = document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement | null;\n if (!el) {\n el = document.createElement(\"style\");\n el.id = STYLE_ELEMENT_ID;\n document.head.appendChild(el);\n }\n return el;\n}\n\nfunction toCssSource(source: string | ArrayBuffer | Blob, format?: string): string {\n if (typeof source === \"string\") {\n const fmt = format ? ` format(\"${format}\")` : \"\";\n return `url(\"${source}\")${fmt}`;\n }\n const blob = source instanceof Blob ? source : new Blob([source]);\n const url = URL.createObjectURL(blob);\n const fmt = format ? ` format(\"${format}\")` : \"\";\n return `url(\"${url}\")${fmt}`;\n}\n\n/**\n * 注册一个公文字体到指定角色。注册后该角色立即使用此字体(最高优先级)。\n *\n * @example\n * registerFont({ role: \"fangsong\", family: \"FangSong_GB2312\", source: \"/fonts/fs.woff2\", format: \"woff2\" });\n */\nexport function registerFont(options: RegisterFontOptions): void {\n if (typeof document === \"undefined\") {\n throw new Error(\"registerFont 只能在浏览器环境调用。\");\n }\n const { role, family, source, format, weight = \"normal\", target } = options;\n\n if (source) {\n const style = ensureStyleElement();\n const face = `@font-face{font-family:\"${family}\";src:${toCssSource(\n source,\n format,\n )};font-weight:${weight};font-display:swap;}`;\n style.appendChild(document.createTextNode(face));\n }\n\n // 将该角色的 CSS 变量指向新字体,并保留原兜底栈\n const root = target ?? document.documentElement;\n const prev = getComputedStyle(root).getPropertyValue(FONT_CSS_VAR[role]).trim();\n const next = prev ? `\"${family}\", ${prev}` : `\"${family}\"`;\n root.style.setProperty(FONT_CSS_VAR[role], next);\n}\n","/**\n * 分页预览渲染器(浏览器)。\n *\n * 以真实 DOM 测量为准,将公文内容按版心高度逐块流入 A4 页面,并按 GB/T 9704\n * 规则编排页码(单页居右空一字、双页居左空一字)。用于打印/导出前的所见即所得预览。\n *\n * v1 以“整块”为流动粒度(不在段落中间断行);超长段落的跨页断行将随\n * 编辑器内联分页一并在后续迭代提供。headless paginate() 已支持按行精确分页,\n * 可用于页数与导出计算。\n */\nimport type { JSONContent } from \"@tiptap/core\";\nimport { MARGIN_MM, TYPE_AREA_MM } from \"../spec/layout\";\nimport type { OfficialElement } from \"../spec/elements\";\nimport { injectOfficialStyles } from \"../styles/inject\";\nimport { pageNumberStyle, PAGE_NUMBER_OFFSET_MM } from \"../pagination/page-number\";\n\nconst MM_TO_PX = 96 / 25.4;\nconst mm = (v: number): string => `${v}mm`;\n\nexport interface PaginatedPreviewOptions {\n /** 是否渲染页码,默认 true */\n showPageNumber?: boolean;\n}\n\nfunction renderBlock(node: JSONContent): HTMLElement {\n if (node.type === \"horizontalRule\") {\n const hr = document.createElement(\"hr\");\n const variant = (node.attrs?.variant as string) || \"reverse\";\n hr.className = `odoc-separator odoc-hr--${variant}`;\n return hr;\n }\n if (node.type === \"image\") {\n const img = document.createElement(\"img\");\n if (node.attrs?.src) img.src = String(node.attrs.src);\n if (node.attrs?.alt) img.alt = String(node.attrs.alt);\n if (node.attrs?.seal) img.className = \"odoc-seal\";\n return img;\n }\n if (node.type === \"table\") {\n const table = document.createElement(\"table\");\n for (const row of node.content ?? []) {\n const tr = document.createElement(\"tr\");\n for (const cell of row.content ?? []) {\n const td = document.createElement(cell.type === \"tableHeader\" ? \"th\" : \"td\");\n const text = (cell.content ?? [])\n .map((p) => (p.content ?? []).map((t) => t.text ?? \"\").join(\"\"))\n .join(\"\\n\");\n td.textContent = text;\n tr.appendChild(td);\n }\n table.appendChild(tr);\n }\n return table;\n }\n const p = document.createElement(\"p\");\n const role = node.attrs?.officialRole as OfficialElement | undefined;\n if (role) {\n p.className = `odoc-el odoc-el--${role}`;\n p.dataset.odocRole = role;\n }\n const text = (node.content ?? [])\n .map((n) => (n.type === \"text\" ? (n.text ?? \"\") : \"\"))\n .join(\"\");\n // 空段落需占位高度\n p.textContent = text.length ? text : \" \";\n return p;\n}\n\nfunction createPageNumber(pageNo: number): HTMLElement {\n const style = pageNumberStyle(pageNo);\n const el = document.createElement(\"div\");\n el.className = \"odoc-page-number\";\n el.textContent = style.text;\n el.style.position = \"absolute\";\n // 距版心下边缘 7mm,即距页面底边 (地脚 - 7mm)\n el.style.bottom = mm(MARGIN_MM.bottom - PAGE_NUMBER_OFFSET_MM);\n if (style.align === \"right\") {\n el.style.right = mm(MARGIN_MM.right + style.insetMm);\n } else {\n el.style.left = mm(MARGIN_MM.left + style.insetMm);\n }\n return el;\n}\n\ninterface PageRefs {\n page: HTMLElement;\n area: HTMLElement;\n}\n\n/**\n * 将文档渲染为分页预览,挂载到 mount 容器。返回总页数。\n */\nexport function renderPaginatedPreview(\n doc: JSONContent,\n mount: HTMLElement,\n options: PaginatedPreviewOptions = {},\n): number {\n if (typeof document === \"undefined\") {\n throw new Error(\"renderPaginatedPreview 只能在浏览器环境调用。\");\n }\n const { showPageNumber = true } = options;\n injectOfficialStyles();\n\n mount.innerHTML = \"\";\n mount.classList.add(\"odoc-canvas\");\n\n const pageContentPx = TYPE_AREA_MM.height * MM_TO_PX;\n const blocks = (doc.type === \"doc\" ? doc.content : [doc]) ?? [];\n\n let pageNo = 0;\n let refs: PageRefs;\n\n const newPage = (): PageRefs => {\n pageNo += 1;\n const page = document.createElement(\"div\");\n page.className = \"odoc-page\";\n const area = document.createElement(\"div\");\n area.className = \"odoc-typearea\";\n page.appendChild(area);\n if (showPageNumber) page.appendChild(createPageNumber(pageNo));\n mount.appendChild(page);\n return { page, area };\n };\n\n refs = newPage();\n let used = 0;\n\n for (const node of blocks) {\n const el = renderBlock(node);\n refs.area.appendChild(el);\n const h = el.getBoundingClientRect().height;\n // 按块高累计判断是否超出版心;不依赖容器 min-height(其等于版心高会导致误判)\n if (used + h > pageContentPx && refs.area.childElementCount > 1) {\n refs.area.removeChild(el);\n refs = newPage();\n refs.area.appendChild(el);\n used = el.getBoundingClientRect().height;\n } else {\n used += h;\n }\n }\n\n return pageNo;\n}\n"],"mappings":"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","names":["Extension","Extension","Extension","STYLE_ELEMENT_ID","fmt","MM_TO_PX","text","p"]}
@@ -0,0 +1,131 @@
1
+ import { O as OfficialElement } from '../elements-Bvueu_TD.js';
2
+ import { JSONContent } from '@tiptap/core';
3
+
4
+ /**
5
+ * Headless 分页引擎(GB/T 9704-2012「每面 22 行」模型)。
6
+ *
7
+ * 纯函数、零 DOM 依赖:给定公文各块(段落/分隔线等)及其要素角色与文字,
8
+ * 依版心高度与各要素字号,按“行”为粒度精确计算分页结果。
9
+ * 可用于:页数计算、打印/导出分页、服务端校验,亦为编辑器可视化分页提供基准。
10
+ *
11
+ * 说明:浏览器内的所见即所得分页应以真实 DOM 测量为准(见 PaginationView),
12
+ * 本引擎给出与规范一致的确定性估算,二者模型一致、结果相互印证。
13
+ */
14
+
15
+ /** 版心高度(pt)。 */
16
+ declare const TYPE_AREA_HEIGHT_PT: number;
17
+ /** 正文标准行高(pt):版心高 / 每面行数。 */
18
+ declare const BODY_LINE_HEIGHT_PT: number;
19
+ /** 待分页的块(与 Tiptap JSON 的顶层节点对应)。 */
20
+ interface PaginationBlock {
21
+ /** 公文要素角色;缺省按正文处理 */
22
+ role?: OfficialElement;
23
+ /** 块内纯文本(用于估算行数);分隔线等可为空 */
24
+ text?: string;
25
+ /** 是否为不可断块(如分隔线、表格),不跨页拆分 */
26
+ atomic?: boolean;
27
+ }
28
+ /** 分页结果中某页承载的一个片段。 */
29
+ interface PageFragment {
30
+ /** 对应输入块的下标 */
31
+ blockIndex: number;
32
+ role: OfficialElement;
33
+ /** 该片段在本块中的起始行(从 0 计) */
34
+ startLine: number;
35
+ /** 该片段在本块中的结束行(不含) */
36
+ endLine: number;
37
+ }
38
+ interface Page {
39
+ /** 页码,从 1 开始 */
40
+ pageNo: number;
41
+ fragments: PageFragment[];
42
+ /** 本页已用行高合计(pt) */
43
+ usedHeightPt: number;
44
+ }
45
+ interface PaginateOptions {
46
+ /** 版心可用高度(pt),默认按规范 225mm。 */
47
+ pageHeightPt?: number;
48
+ }
49
+ /** 某要素单行的行高(pt):取正文行高与字号约 1.2 倍中的较大者。 */
50
+ declare function lineHeightPtFor(role: OfficialElement): number;
51
+ /** 某要素每行可容纳的字数(按字号相对版心宽度估算)。 */
52
+ declare function charsPerLineFor(role: OfficialElement): number;
53
+ /** 估算某块的折行数(至少 1 行)。 */
54
+ declare function estimateLines(block: PaginationBlock): number;
55
+ /**
56
+ * 执行分页。返回每页承载的片段(块 + 行区间)。
57
+ * 长段落会按行跨页拆分;atomic 块不拆分(放不下则移至下一页)。
58
+ */
59
+ declare function paginate(blocks: PaginationBlock[], options?: PaginateOptions): Page[];
60
+ /** 便捷方法:仅返回总页数。 */
61
+ declare function countPages(blocks: PaginationBlock[], options?: PaginateOptions): number;
62
+
63
+ /** 一字线(占一字宽的横线),用全角破折号字符表示。 */
64
+ declare const DASH = "\u2014";
65
+ /** 页码与版心下边缘的距离(毫米)。 */
66
+ declare const PAGE_NUMBER_OFFSET_MM = 7;
67
+ type PageNumberAlign = "left" | "right";
68
+ /** 格式化页码,如 1 → “— 1 —”。 */
69
+ declare function formatPageNumber(pageNo: number): string;
70
+ /**
71
+ * 页码对齐方式:单页(奇数)居右空一字,双页(偶数)居左空一字。
72
+ * @param pageNo 从 1 开始的页码
73
+ */
74
+ declare function pageNumberAlign(pageNo: number): PageNumberAlign;
75
+ interface PageNumberStyle {
76
+ text: string;
77
+ align: PageNumberAlign;
78
+ /** 距版心下边缘的距离(毫米) */
79
+ offsetMm: number;
80
+ /** “空一字”的留白(毫米),等于版心每字网格宽度 */
81
+ insetMm: number;
82
+ }
83
+ /** 给定页码,返回其完整排布信息(文本/对齐/偏移/留白)。 */
84
+ declare function pageNumberStyle(pageNo: number): PageNumberStyle;
85
+
86
+ /**
87
+ * 从 Tiptap/ProseMirror JSON 文档抽取分页所需的块信息。
88
+ */
89
+
90
+ /** 将 doc 的顶层节点转为分页块列表。 */
91
+ declare function blocksFromDoc(doc: JSONContent): PaginationBlock[];
92
+
93
+ /**
94
+ * 断页几何(headless,纯函数,可单测)。
95
+ *
96
+ * 输入各顶层块在“连续排布”下的自然位置(top/height,单位 px),输出应在何处插入
97
+ * 分页间隔(spacer)以将内容可视化为多页。被编辑器内联分页插件用于驱动装饰渲染。
98
+ *
99
+ * v1 以块为断页粒度:跨页边界的块整体移至下一页;超过整页高度的单块允许溢出
100
+ * (留待后续按行拆分)。
101
+ */
102
+ interface BlockRect {
103
+ /** 块在编辑区内的自然顶端(相对编辑区顶部,px) */
104
+ top: number;
105
+ /** 块高度(px) */
106
+ height: number;
107
+ }
108
+ interface PageBreakOptions {
109
+ /** 每页版心可用内容高度(px) */
110
+ pageContentPx: number;
111
+ /** 两页之间的视觉间隔(px):上一页地脚 + 页间距 + 下一页天头 */
112
+ breakExtraPx: number;
113
+ }
114
+ interface PageBreak {
115
+ /** 在第几个块之前插入分隔(块下标) */
116
+ beforeIndex: number;
117
+ /** 新页的页码(从 2 起) */
118
+ pageNo: number;
119
+ /** 需要插入的间隔高度(px) */
120
+ spacerPx: number;
121
+ }
122
+ interface PageBreakResult {
123
+ breaks: PageBreak[];
124
+ pageCount: number;
125
+ }
126
+ /**
127
+ * 计算分页断点。
128
+ */
129
+ declare function computePageBreaks(blocks: BlockRect[], options: PageBreakOptions): PageBreakResult;
130
+
131
+ export { BODY_LINE_HEIGHT_PT, type BlockRect, DASH, PAGE_NUMBER_OFFSET_MM, type Page, type PageBreak, type PageBreakOptions, type PageBreakResult, type PageFragment, type PageNumberAlign, type PageNumberStyle, type PaginateOptions, type PaginationBlock, TYPE_AREA_HEIGHT_PT, blocksFromDoc, charsPerLineFor, computePageBreaks, countPages, estimateLines, formatPageNumber, lineHeightPtFor, pageNumberAlign, pageNumberStyle, paginate };
@@ -0,0 +1,34 @@
1
+ import {
2
+ BODY_LINE_HEIGHT_PT,
3
+ DASH,
4
+ PAGE_NUMBER_OFFSET_MM,
5
+ TYPE_AREA_HEIGHT_PT,
6
+ blocksFromDoc,
7
+ charsPerLineFor,
8
+ computePageBreaks,
9
+ countPages,
10
+ estimateLines,
11
+ formatPageNumber,
12
+ lineHeightPtFor,
13
+ pageNumberAlign,
14
+ pageNumberStyle,
15
+ paginate
16
+ } from "../chunk-OUPZEHJO.js";
17
+ import "../chunk-SYMAPCOS.js";
18
+ export {
19
+ BODY_LINE_HEIGHT_PT,
20
+ DASH,
21
+ PAGE_NUMBER_OFFSET_MM,
22
+ TYPE_AREA_HEIGHT_PT,
23
+ blocksFromDoc,
24
+ charsPerLineFor,
25
+ computePageBreaks,
26
+ countPages,
27
+ estimateLines,
28
+ formatPageNumber,
29
+ lineHeightPtFor,
30
+ pageNumberAlign,
31
+ pageNumberStyle,
32
+ paginate
33
+ };
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,69 @@
1
+ export { C as ChineseFontSizeName, E as ELEMENT_SPEC, a as ElementSpec, b as FONT_CSS_VAR, c as FONT_SIZE_PT, d as FONT_STACK, F as FontRole, e as OFFICIAL_RED, O as OfficialElement, P as PT_TO_MM, f as PT_TO_PX, p as ptToMm, g as ptToPx, t as toHalfPoint, h as toPt } from '../elements-Bvueu_TD.js';
2
+
3
+ /**
4
+ * GB/T 9704-2012《党政机关公文格式》页面与版心规范。
5
+ *
6
+ * 标准要点(A4 纸,210mm × 297mm):
7
+ * - 版心尺寸:156mm(宽)× 225mm(高)。
8
+ * - 页边距:天头(上白边)37mm,订口(左白边)28mm;
9
+ * 由版心尺寸反推:地脚(下白边)35mm,切口(右白边)26mm。
10
+ * - 正文每面排 22 行、每行 28 个字(3 号仿宋)。
11
+ * - 字间距与行距由版心尺寸均分得到,保证每面行数、每行字数固定。
12
+ */
13
+ /** A4 纸张尺寸(毫米)。 */
14
+ declare const PAGE_A4_MM: {
15
+ readonly width: 210;
16
+ readonly height: 297;
17
+ };
18
+ /** 版心尺寸(毫米)。 */
19
+ declare const TYPE_AREA_MM: {
20
+ readonly width: 156;
21
+ readonly height: 225;
22
+ };
23
+ /**
24
+ * 页边距(毫米)。
25
+ * top = 天头(上白边),left = 订口(左白边),其余由版心尺寸反推。
26
+ */
27
+ declare const MARGIN_MM: {
28
+ readonly top: 37;
29
+ readonly left: 28;
30
+ readonly right: number;
31
+ readonly bottom: number;
32
+ };
33
+ /** 正文每面行数。 */
34
+ declare const LINES_PER_PAGE = 22;
35
+ /** 正文每行字数。 */
36
+ declare const CHARS_PER_LINE = 28;
37
+ /**
38
+ * 正文标准行距(毫米):版心高度 / 每面行数。
39
+ * 225 / 22 ≈ 10.227mm ≈ 28.98pt,实务中常设为固定行距 28.8~29pt。
40
+ */
41
+ declare const LINE_HEIGHT_MM: number;
42
+ /**
43
+ * 正文每字标准网格宽度(毫米):版心宽度 / 每行字数。
44
+ * 156 / 28 ≈ 5.571mm。
45
+ */
46
+ declare const CHAR_BOX_MM: number;
47
+ declare const Layout: {
48
+ readonly page: {
49
+ readonly width: 210;
50
+ readonly height: 297;
51
+ };
52
+ readonly typeArea: {
53
+ readonly width: 156;
54
+ readonly height: 225;
55
+ };
56
+ readonly margin: {
57
+ readonly top: 37;
58
+ readonly left: 28;
59
+ readonly right: number;
60
+ readonly bottom: number;
61
+ };
62
+ readonly linesPerPage: 22;
63
+ readonly charsPerLine: 28;
64
+ readonly lineHeightMm: number;
65
+ readonly charBoxMm: number;
66
+ };
67
+ type LayoutSpec = typeof Layout;
68
+
69
+ export { CHARS_PER_LINE, CHAR_BOX_MM, LINES_PER_PAGE, LINE_HEIGHT_MM, Layout, type LayoutSpec, MARGIN_MM, PAGE_A4_MM, TYPE_AREA_MM };
@@ -0,0 +1,45 @@
1
+ import {
2
+ FONT_CSS_VAR,
3
+ FONT_STACK
4
+ } from "../chunk-TRNXHJAU.js";
5
+ import {
6
+ CHARS_PER_LINE,
7
+ CHAR_BOX_MM,
8
+ ELEMENT_SPEC,
9
+ FONT_SIZE_PT,
10
+ LINES_PER_PAGE,
11
+ LINE_HEIGHT_MM,
12
+ Layout,
13
+ MARGIN_MM,
14
+ OFFICIAL_RED,
15
+ PAGE_A4_MM,
16
+ PT_TO_MM,
17
+ PT_TO_PX,
18
+ TYPE_AREA_MM,
19
+ ptToMm,
20
+ ptToPx,
21
+ toHalfPoint,
22
+ toPt
23
+ } from "../chunk-SYMAPCOS.js";
24
+ export {
25
+ CHARS_PER_LINE,
26
+ CHAR_BOX_MM,
27
+ ELEMENT_SPEC,
28
+ FONT_CSS_VAR,
29
+ FONT_SIZE_PT,
30
+ FONT_STACK,
31
+ LINES_PER_PAGE,
32
+ LINE_HEIGHT_MM,
33
+ Layout,
34
+ MARGIN_MM,
35
+ OFFICIAL_RED,
36
+ PAGE_A4_MM,
37
+ PT_TO_MM,
38
+ PT_TO_PX,
39
+ TYPE_AREA_MM,
40
+ ptToMm,
41
+ ptToPx,
42
+ toHalfPoint,
43
+ toPt
44
+ };
45
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * 公文页面与版心基础样式(GB/T 9704-2012)。
3
+ * A4 210×297mm,版心 156×225mm,天头 37mm、订口 28mm、切口 26mm、地脚 35mm。
4
+ *
5
+ * 要素级样式(字体/字号/对齐/缩进)由 injectOfficialStyles() 依 ELEMENT_SPEC 注入,
6
+ * 此处只负责页面、版心、字体兜底变量与可视化网格。
7
+ */
8
+
9
+ :root {
10
+ --odoc-font-fangsong: "仿宋_GB2312", "仿宋", "FangSong", "STFangsong",
11
+ "Source Han Serif SC", "Noto Serif SC", serif;
12
+ --odoc-font-xiaobiaosong: "方正小标宋简体", "STZhongsong", "华文中宋",
13
+ "Source Han Serif SC", "Noto Serif SC", serif;
14
+ --odoc-font-heiti: "黑体", "SimHei", "STHeiti", "Source Han Sans SC",
15
+ "Noto Sans SC", sans-serif;
16
+ --odoc-font-kaiti: "楷体_GB2312", "楷体", "KaiTi", "STKaiti",
17
+ "Source Han Serif SC", "Noto Serif SC", serif;
18
+ --odoc-font-songti: "宋体", "SimSun", "STSong", "Source Han Serif SC",
19
+ "Noto Serif SC", serif;
20
+
21
+ --odoc-page-bg: #fff;
22
+ --odoc-canvas-bg: #f0f1f3;
23
+ --odoc-red: #e60012;
24
+ }
25
+
26
+ /* 灰色画布,居中摆放页面 */
27
+ .odoc-canvas {
28
+ background: var(--odoc-canvas-bg);
29
+ padding: 24px;
30
+ display: flex;
31
+ flex-direction: column;
32
+ align-items: center;
33
+ gap: 24px;
34
+ overflow: auto;
35
+ }
36
+
37
+ /* 一张 A4 纸 */
38
+ .odoc-page {
39
+ position: relative;
40
+ box-sizing: border-box;
41
+ width: 210mm;
42
+ min-height: 297mm;
43
+ background: var(--odoc-page-bg);
44
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
45
+ /* 天头/订口/切口/地脚 = 版心外的四周白边 */
46
+ padding: 37mm 26mm 35mm 28mm;
47
+ }
48
+
49
+ /* 版心:可编辑区域 */
50
+ .odoc-page .ProseMirror,
51
+ .odoc-typearea {
52
+ box-sizing: border-box;
53
+ width: 156mm; /* 版心宽 */
54
+ min-height: 225mm; /* 版心高 */
55
+ outline: none;
56
+ /* 正文默认:3 号仿宋,固定行距 ≈ 28.8pt(每面 22 行) */
57
+ font-family: var(--odoc-font-fangsong);
58
+ font-size: 16pt;
59
+ line-height: 28.8pt;
60
+ color: #000;
61
+ }
62
+
63
+ .odoc-page .ProseMirror p,
64
+ .odoc-typearea p {
65
+ margin: 0;
66
+ }
67
+
68
+ /* 图片:居中、限定版心宽度(编辑区与预览区共用) */
69
+ .odoc-typearea img {
70
+ display: block;
71
+ max-width: 100%;
72
+ height: auto;
73
+ margin: 0.3em auto;
74
+ }
75
+
76
+ /* 印章:红色叠加于成文日期之上(以负外边距上移叠压,混合模式取红) */
77
+ .odoc-typearea img.odoc-seal {
78
+ position: relative;
79
+ z-index: 2;
80
+ margin-top: -2.2em;
81
+ max-width: 40mm;
82
+ mix-blend-mode: multiply;
83
+ pointer-events: none;
84
+ }
85
+
86
+ /* 公文表格:全框线、仿宋、单元格内容居中(编辑区与预览区共用) */
87
+ .odoc-typearea table {
88
+ border-collapse: collapse;
89
+ width: 100%;
90
+ margin: 0.4em 0;
91
+ table-layout: fixed;
92
+ }
93
+
94
+ .odoc-typearea th,
95
+ .odoc-typearea td {
96
+ border: 1px solid #000;
97
+ padding: 2px 6px;
98
+ vertical-align: middle;
99
+ font-family: var(--odoc-font-fangsong);
100
+ font-size: 16pt; /* 三号 */
101
+ white-space: pre-wrap;
102
+ }
103
+
104
+ .odoc-typearea th {
105
+ font-family: var(--odoc-font-heiti);
106
+ text-align: center;
107
+ }
108
+
109
+ /* 分隔线:默认红头反线(红色);版记线为黑色全幅。与版心等宽 */
110
+ .odoc-page .ProseMirror hr,
111
+ .odoc-separator {
112
+ border: none;
113
+ border-top: 1.5pt solid var(--odoc-red);
114
+ margin: 0.4em 0;
115
+ }
116
+
117
+ .odoc-page .ProseMirror hr.odoc-hr--record,
118
+ .odoc-separator.odoc-hr--record {
119
+ border-top: 0.75pt solid #000;
120
+ }
121
+
122
+ /* 可选:显示版心边界,便于核对版式 */
123
+ .odoc-page--show-grid .ProseMirror {
124
+ outline: 1px dashed #c9ccd1;
125
+ outline-offset: 0;
126
+ }
127
+
128
+ /* 占位提示 */
129
+ .odoc-page .ProseMirror p.is-editor-empty:first-child::before {
130
+ content: attr(data-placeholder);
131
+ color: #adb5bd;
132
+ float: left;
133
+ height: 0;
134
+ pointer-events: none;
135
+ }
136
+
137
+ /* 页码:4 号半角宋体阿拉伯数字,编排于版心下边缘之下 7mm */
138
+ .odoc-page-number {
139
+ font-family: var(--odoc-font-songti);
140
+ font-size: 14pt; /* 四号 */
141
+ color: #000;
142
+ white-space: nowrap;
143
+ }
144
+
145
+ /* 编辑器内联分页:分页间隔。纵向渐变还原“上一页地脚(白)→页间空隙(灰)→下一页天头(白)”,
146
+ 页码落在上一页版心下边缘之下。背景渐变由插件按各页剩余高度内联设置。 */
147
+ .odoc-page-break {
148
+ position: relative;
149
+ margin: 0 calc(-1 * 26mm) 0 calc(-1 * 28mm); /* 全幅,抵消版心左右白边 */
150
+ user-select: none;
151
+ }
152
+
153
+ .odoc-inline-page-number {
154
+ font-family: var(--odoc-font-songti);
155
+ font-size: 14pt; /* 四号 */
156
+ color: #000;
157
+ white-space: nowrap;
158
+ }
159
+
160
+ /* 打印:每页一张 A4,去除画布背景与阴影,页码随页输出 */
161
+ @media print {
162
+ @page {
163
+ size: A4;
164
+ margin: 0;
165
+ }
166
+
167
+ html,
168
+ body {
169
+ background: #fff;
170
+ }
171
+
172
+ .odoc-canvas {
173
+ background: #fff;
174
+ padding: 0;
175
+ gap: 0;
176
+ display: block;
177
+ }
178
+
179
+ .odoc-page {
180
+ box-shadow: none;
181
+ break-after: page;
182
+ page-break-after: always;
183
+ }
184
+
185
+ .odoc-page:last-child {
186
+ break-after: auto;
187
+ page-break-after: auto;
188
+ }
189
+ }
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@maxoyed/ode-core",
3
+ "version": "0.0.1",
4
+ "description": "Headless 公文编辑器核心:GB/T 9704-2012 版式规范、Tiptap 公文节点与字体插槽,与前端框架无关。",
5
+ "license": "MIT",
6
+ "author": "maxoyed <y@openingsource.org>",
7
+ "homepage": "https://github.com/maxoyed/official-document-editor#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/maxoyed/official-document-editor.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/maxoyed/official-document-editor/issues"
15
+ },
16
+ "keywords": [
17
+ "公文",
18
+ "公文编辑器",
19
+ "official-document",
20
+ "government",
21
+ "GB/T 9704",
22
+ "tiptap",
23
+ "prosemirror",
24
+ "docx",
25
+ "headless",
26
+ "editor",
27
+ "pagination"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "type": "module",
33
+ "main": "./dist/index.js",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js"
40
+ },
41
+ "./styles.css": "./dist/styles.css",
42
+ "./spec": {
43
+ "types": "./dist/spec/index.d.ts",
44
+ "import": "./dist/spec/index.js"
45
+ },
46
+ "./pagination": {
47
+ "types": "./dist/pagination/index.d.ts",
48
+ "import": "./dist/pagination/index.js"
49
+ },
50
+ "./docx": {
51
+ "types": "./dist/docx/index.d.ts",
52
+ "import": "./dist/docx/index.js"
53
+ }
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "README.md",
58
+ "LICENSE"
59
+ ],
60
+ "sideEffects": [
61
+ "**/*.css"
62
+ ],
63
+ "dependencies": {
64
+ "@tiptap/core": "^2.11.5",
65
+ "@tiptap/extension-color": "^2.11.5",
66
+ "@tiptap/extension-image": "^2.27.2",
67
+ "@tiptap/extension-table": "^2.27.2",
68
+ "@tiptap/extension-table-cell": "^2.27.2",
69
+ "@tiptap/extension-table-header": "^2.27.2",
70
+ "@tiptap/extension-table-row": "^2.27.2",
71
+ "@tiptap/extension-text-align": "^2.11.5",
72
+ "@tiptap/extension-text-style": "^2.11.5",
73
+ "@tiptap/pm": "^2.11.5",
74
+ "@tiptap/starter-kit": "^2.11.5",
75
+ "docx": "^9.7.1",
76
+ "fast-xml-parser": "^5.8.0",
77
+ "fflate": "^0.8.3"
78
+ },
79
+ "devDependencies": {
80
+ "tsup": "^8.3.5",
81
+ "typescript": "^5.7.3"
82
+ },
83
+ "scripts": {
84
+ "build": "tsup",
85
+ "dev": "tsup --watch",
86
+ "typecheck": "tsc --noEmit",
87
+ "test": "tsup && node --test --experimental-strip-types test/*.test.ts"
88
+ }
89
+ }