- 前端
- 富文本
- UI组件
- AI 应用
【免费下载链接】BlockNote
A React Rich Text Editor that's block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.
导读
本文围绕 BlockNote 仓库中packages/xl-odt-exporter/src/odt/template/模板目录展开,说明 ODT(OpenDocument Text)导出器如何以一份由 LibreOffice 生成的演示文档为模板、并最终只消费其中的styles.xml的完整设计。读完本文,你将掌握 ODT 导出包内部的文件结构、模板的由来与格式化策略、以及导出器基于 React 与 zip.js 组装 ODF 文档的底层实现。
模板目录是什么:一份"被拆开"的 ODT 文件
ODF(OpenDocument)本质上是一个 ZIP 容器,里面装着若干 XML 与二进制资源。BlockNote 的 ODT 导出器并不从零手写整套 ODF 样式体系,而是先让 LibreOffice 生成一份"样板"文档,再把它解压、格式化后放进仓库作为模板。
模板目录packages/xl-odt-exporter/src/odt/template/中的README.md明确记录了三条关键信息:
template blocknote.odt是演示 docx 导出的产物:它源自仓库中"将块转换为 docx"的示例(对应 examples/05-interoperability/06-converting-blocks-to-docx),先在 Mac 上用 LibreOffice 打开该 docx,再"另存为 ODT"得到。- 解压出的文件都经过了格式化:用 VS Code 的 XML 格式化器重新排版,目的是让跨提交的 diff 可读——未格式化的 LibreOffice XML 通常是单行超长文本,无法进行版本对比。
styles.xml是导出器唯一实际使用的文件:其余文件(content.xml、meta.xml、settings.xml等)只是模板工作流的副产品,导出器运行时会重新生成自己的版本。
这一"以真实办公软件产物为模板"的做法,保证了导出的 ODT 在 LibreOffice/Word 中的样式基线(字体、段落、标题、列表、边框等)与真实文档一致,而不是导出器闭门造车。
模板目录结构逐文件解析
解压template blocknote.odt后得到如下结构:
packages/xl-odt-exporter/src/odt/template/ ├── META-INF/ │ └── manifest.xml # ODF 包内文件清单(模板自身版本) ├── Pictures/ │ └── 100000000000014C0000014CDD284996.jpg # 模板内嵌的演示图片 ├── Thumbnails/ │ └── thumbnail.png # 文档缩略图 ├── content.xml # 文档正文(430 行,模板自身版本) ├── manifest.rdf # RDF 元数据 ├── meta.xml # 文档元信息 ├── mimetype # 固定文本:application/vnd.oasis.opendocument.text ├── settings.xml # 编辑器视图设置 ├── styles.xml # 样式定义(1078 行,导出器唯一消费的文件) └── template blocknote.odt # 原始 ODT 二进制ODF 规范要求 ZIP 包内第一个条目必须是mimetype,且不压缩存储——这是所有 ODF 阅读器识别文件类型的依据。模板目录完整保留了这一约定,而导出器在运行时也会严格遵守(见下文)。
从 manifest.xml 可以看到模板自带的清单声明了content.xml、styles.xml、Pictures/等全部条目;这份清单同样是"模板自身版本",导出时会用动态生成的 manifest 取代它。
styles.xml:导出器唯一真正消费的模板资产
README.md强调的第三点,在源码中得到直接印证:odtExporter.tsx 第 17 行 通过 Vite 的?raw导入把模板中的styles.xml以纯文本方式打进产物:
import stylesXml from "./template/styles.xml?raw";随后在toODTDocument的打包阶段,这段 XML 原样写入 ZIP 包:
void zipWriter.add("styles.xml", new TextReader(stylesXml));也就是说,无论用户文档包含多少块、多少样式,styles.xml始终是模板里那份固定的 1078 行文件,它提供了:默认字体声明(Inter 18pt、Geist Mono、PingFang SC 等)、style:default-style(graphic / paragraph / text 三类默认样式)、标题样式Heading_20_1~Heading_20_6、列表样式WWNum1、No_20_List、代码块样式Codeblock、Caption、PageBreak、Internet_20_link等命名样式。
从源码结构可以推断其分工:
- 静态命名样式(标题、列表、链接、代码块、页眉页脚等)由
styles.xml预先定义; - 动态自动样式(由用户块属性生成)由导出器在运行时写入
content.xml的<office:automatic-styles>节,两者互不干扰。
这也是"模板 + 运行时生成"双轨设计:模板负责稳定的排版基线,运行时负责逐文档的个性化属性。
导出器如何组装一个合法的 ODT 包
ODTExporter继承自@blocknote/core的Exporter基类,核心方法toODTDocument(odtExporter.tsx L190-L375)用@zip.js/zip.js的ZipWriter按顺序写入条目:
mimetype:第一项,compressionMethod: 0(不压缩),值固定为application/vnd.oasis.opendocument.text,严格遵循 ODF 规范;content.xml:由 React 组件树经renderToString序列化得到(<office:document-content office:version="1.3">,内含<office:font-face-decls>、<office:automatic-styles>、可选的<office:master-styles>(页眉/页脚)与<office:body>);styles.xml:来自模板的?raw导入;META-INF/manifest.xml:动态生成的清单,声明根条目、content.xml、styles.xml,以及随后追加的每张图片、每个字体与每个嵌入对象;Fonts/*:loadFonts从@shared/assets/fonts/载入 Inter 18pt 与 Geist Mono 两个 TTF,写入Fonts/目录并在font-face-decls中声明;Pictures/*:图片经registerPicture注册后按Pictures/picture-N.ext命名写入;Object N/content.xml:公式等嵌入对象的子文档(见下文)。
页眉页脚与命名空间清理
toODTDocument的第二个参数支持传入header/footer(字符串或XMLDocument)。传入的 XML 会经xmlOptionToString处理:用正则剥离掉已在根元素声明过的重复命名空间声明,再注入<style:master-page style:name="Standard">的<style:header>/<style:footer>中。对应测试见 odtExporter.test.ts 的 "should export a document with custom document options"。
自动样式注册与去重:BN_S / BN_T 命名空间
ODF 的样式分为命名样式与自动样式两类。导出器对"由块属性临时派生"的样式通过registerStyle(odtExporter.tsx L377-L391)注册到<office:automatic-styles>:
- 样式名形如
BN_S{n}(段落/表格等),按注册顺序自增; - 按渲染形状去重:以占位符名字渲染出样式定义、用
JSON.stringify作为键存入registeredStyleNames,相同定义复用同一名字——否则一份多段落文档会把 automatic-styles 塞满重复拷贝; - 内联文本样式同理,
transformStyledText以T:前缀的键去重,命名BN_T{n}(odtExporter.tsx L128-L139)。
测试 “deduplicates identical automatic styles” 验证了同一 italic 定义两次注册返回相同名字、italic 与 bold 返回不同名字。
块级样式的生成集中在 blocks.tsx 的createParagraphStyle:把 BlockNote 的textAlignment(left/center/right/justify)映射为fo:text-align的 start/center/end/justify;把backgroundColor/textColor通过exporter.options.colors(默认COLORS_DEFAULT)解析成十六进制色值;全部属性为空时直接回退到父样式Standard,避免产生无意义的新样式。引用块、分隔线则通过参数注入fo:border-left、fo:border-top等属性形成视觉样式。
图片、字体与嵌入对象
registerPicture(odtExporter.tsx L417-L462)负责图片资源:
- 同一 URL 幂等(
picturesMap 缓存),重复引用复用同一文件; - 用
resolveFileUrl拉取图片(默认corsProxyResolveFileUrl,走 CORS 代理,与 pdf/docx 导出器一致); - 依据 Blob 的 MIME 类型推断扩展名(apng/avif/bmp/gif/ico/jpg/png/svg/tiff/webp,未知回退 png);
- 用
getImageDimensions读取原始像素尺寸,供draw:frame的svg:width/svg:height使用。
registerObject(odtExporter.tsx L408-L415)支持公式等嵌入对象:注册后返回Object N/路径,导出时写入Object N/content.xml子文档,并在 manifest 中声明其 media-type(默认application/vnd.oasis.opendocument.formula),块映射中可通过draw:object的xlink:href引用。
默认 Schema 映射:块到 ODF 的翻译
defaultSchema/index.ts 汇出odtDefaultSchemaMappings,由 blocks.tsx(块映射)、inlineContent.tsx(行内内容)、styles.ts(样式映射)组成。关键翻译规则包括:
- 段落/标题/引用:映射为
text:p/text:h,嵌套层级用text:tab前缀缩进(getTabs); - 列表:每个列表项各自包一层
text:list,注释说明这是与 Word DocX→ODT 导出一致的做法,LibreOffice 打开后会自行合并为同一列表;编号列表通过text:continue-numbering与text:start-value延续编号; - 待办项:用
☒/☐前缀文本表示勾选状态;折叠项用>前缀; - 分页符:输出带
PageBreak样式的空段落; - 表格:列宽以 px×0.75 换算为 pt,
style:rel-column-width处理多列布局;合并单元格遵循 ODF 模型——跨越单元格带table:number-columns/rows-spanned,被覆盖的网格位置必须补<table:covered-table-cell/>(blocks.tsx L511-L564 的网格扫描逻辑); - 图片/文件/视频/音频:无 URL 的"未上传占位符"不是文档内容,导出为空;有 URL 的媒体输出为
Internet_20_link链接(dictionary.open_file等文案); - 多列:复用
@blocknote/xl-multi-column的 schema,渲染为 ODF 表格; - 行内样式(styles.ts):bold→
fo:font-weight: bold、italic→fo:font-style: italic、underline→style:text-underline-style: solid、strike→style:text-line-through-style: solid、textColor/backgroundColor→fo:color/fo:background-color、code→style:font-name: Courier New。
测试与快照:验证模板工作流的闭环
odtExporter.test.ts用testDocument(shared/testDocument.ts)构造覆盖多类块的文档,经testODTDocumentAgainstSnapshot与__snapshots__/basic/、__snapshots__/withCustomOptions/下的content.xml/styles.xml快照对比,验证了:
- 基础导出:包含自动样式、图片、表格等元素的文档能稳定生成合法 ODF;
- 自定义页眉页脚:
toODTDocument的 options 生效; - 样式去重:同一渲染形状的样式只注册一次。
快照与模板目录中格式化的 XML 一脉相承——正是"用 VS Code XML 格式化器排版、保证 diff 可读"策略在测试侧的延续。
小结:模板目录的设计哲学
回顾template/README.md的三条结论,可归纳出 ODT 导出器模板设计的三点核心:
- 以真实产物为起点:用 LibreOffice 打开官方 docx 演示再另存为 ODT,确保样式基线贴近主流办公软件的渲染结果;
- 格式化留痕:解压出的 XML 统一格式化,让模板的每次演进都能通过 git diff 审查;
- 最小消费:1078 行的
styles.xml是唯一被?raw导入并原样写入导出包的模板资产,其余文件(content.xml、meta.xml、manifest.rdf、settings.xml、Thumbnails 等)仅作为工作流见证保留在目录中。
如果要在 BlockNote 基础上定制 ODT 导出,最实用的入口正是修改 template/styles.xml 中的命名样式(如默认字体、标题颜色、列表符号),或扩展 blocks.tsx 的块映射;修改后跑pnpm test(见 package.json)即可用快照校验输出是否符合预期。
- 前端
- 富文本
- UI组件
- AI 应用
【免费下载链接】BlockNote
A React Rich Text Editor that's block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.
相关推荐
BlockNote 文档导出 ODT(Open Document Text)完整实战指南:从编辑器到可下载的 .odt 文件
BlockNote 文档导出 ODT(Open Document Text)完整实战指南:从编辑器到可下载的 .odt 文件 导读 本指南围绕 BlockNot
前端富文本UI组件AI 应用BentoPDF Word to PDF 转换工具全解析:浏览器内 LibreOffice WASM 引擎下的 DOCX/DOC/ODT/RTF 转换指南
BentoPDF Word to PDF 转换工具全解析:浏览器内 LibreOffice WASM 引擎下的 DOCX/DOC/ODT/RTF 转换指南 Be
前端突破文档预览瓶颈:kkFileView集成LibreOffice实现Markdown到ODT无缝转换
突破文档预览瓶颈:kkFileView集成LibreOffice实现Markdown到ODT无缝转换 在日常办公和开发中,你是否遇到过这些问题:上传的Markd
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考