Skip to content

v7.6.1

Choose a tag to compare

@github-actions github-actions released this 14 Aug 21:52
· 13 commits to master since this release

v7.6.1: 🔗 Structured-Editor HTML Interop, Lossless Markdown Round-Trips & Footnote Fidelity

I am pleased to announce the release of officeParser v7.6.1! This release teaches HtmlParser the on-the-wire shapes that structured (Tiptap-style) editors actually emit, and makes a document survive the full save → load → save cycle without quietly losing footnotes, highlights, horizontal rules, or frontmatter types along the way. The guiding rule throughout: expand, never inhibit — every parser change only widens what is accepted, and every new generator behaviour is off by default, so existing output is byte-identical except for the deliberate fixes called out below.

(This release also carries the changes staged as 7.6.0, which was merged to master but never separately published — everything is delivered together here.)

Thanks to @pipaacebedo (#109) and @MohammedAlkindi (#111), whose reports drove the fixes below.

Warning

Behavior changes

  • A DOCX paragraph mark's run properties no longer bleed onto every run. Bold/italic/underline/colour/size/font set on a paragraph mark (<w:pPr><w:rPr>) were folded into the base formatting of all the paragraph's runs. Per OOXML ISO 29500 §17.3.1.29 those properties format only the mark glyph; runs now inherit only from the style chain and their own run properties, so formatting on affected runs differs from prior releases. (#109)
  • Thematic breaks (--- / <hr>) now survive a save. A Markdown --- and an HTML <hr> parsed to a page break, which the Markdown generator emits as a bare newline — so a horizontal rule silently vanished on the first save. It is now a distinct breakType: 'thematic' that emits --- in Markdown and <hr> in HTML; an office page break (<hr class="page-break">) stays a page break.
  • Highlights are generated as <mark>, not <span style="background-color">. Editors whose highlight extension matches only the mark element (e.g. Tiptap's Highlight) now rehydrate a highlighted run that previously came back as plain text. <mark> and data-color are also parsed on import.
  • Footnotes no longer grow a ### Notes heading on every save, and empty metadata no longer corrupts the cycle. The Markdown footnote section is emitted as bare [^id]: definitions (byte-stable across cycles), and a document with no metadata fields no longer emits an ---\n--- block that reparsed as a heading.
  • Frontmatter scalar types are preserved. A quoted version: "123" stays a string across a round trip instead of coercing to a number; only bare scalars coerce (YAML semantics).
  • EmbedMetadata widened. embedType is now 'youtube' | 'iframe', videoId is optional, and a height field is added. Strict TypeScript consumers that read videoId as a non-optional string, or switched exhaustively on embedType, may need a small type adjustment.
  • The footnote-definition HTML is a <div data-footnote-id>, not a <p> wrapping block content (which every DOM parser split). Default footnote-definition markup changes.

🌟 What's New

1. Attribute-Driven HTML Interop for Wikilinks, Citations, Math and Mermaid

HtmlParser now accepts the shapes structured editors serialize, so content authored in an editor round-trips through officeParser instead of flattening to plain text on the way back:

  • a[data-wikilink] — page in data-target, display text from the anchor body or data-alias.
  • span.citation[data-key] — the same bare-key citation node as <cite data-citation-key>.
  • data-math — disambiguated by value: the library's own data-math="inline|block" is read exactly as before, while any other value is taken as the raw LaTeX (previously read as inline math with the attribute ignored).
  • div[data-mermaid] / div.mermaid / pre.mermaid — mapped to a mermaid code node (previously the div flattened to paragraph text).

The complementary emission is opt-in behind one behavior-named key, HtmlGeneratorConfig.sourceAttributes (default false), so an attribute-driven consumer can rehydrate each node from a data-* attribute. Off by default, output is byte-identical; on, the widened parser reads back every shape it emits, so output stays self-round-trippable. Every sink is entity-escaped, and PDF/EPUB generation force the flag off.

// Round-trips cleanly with the editor's own serialization:
const html = String((await ast.to('html', { htmlConfig: { sourceAttributes: true } })).value);
// <a data-wikilink="true" data-target="Page" data-alias="Alias">…</a>
// <span class="citation" data-key="smith2020">…</span>
// <div class="mermaid" data-mermaid="graph TD; A--&gt;B">…</div>

2. Markdown Footnotes That Survive the Round Trip

Footnotes were the single biggest source of quiet corruption on the editor's save/load path, and this release closes every case that surfaced:

  • Multi-line definitions continue across indented lines (Pandoc/GFM) and re-emit indented, instead of being cut short at the first newline.
  • Orphan definitions — a [^x]: … with no matching reference — are preserved on both sides (recovered from .md and from a section[data-footnotes]) with no dangling back-link, so they survive a full md → HTML → md trip instead of being dropped.
  • Repeated references to one id stay [^1]/[^1] with a single definition, rather than renumbering to [^1]/[^2] and duplicating the body. Office notes that merely share a numeric id (a footnote and an endnote both numbered 1) remain distinct.
  • A footnote referenced in a table cell is defined exactly once; the generator no longer double-processes cells and pushes the note twice.
  • Footnote and endnote bodies now reach RAG chunks (office and Markdown origin), folded into the referencing node's chunk text — searchable where they were previously absent.

3. Blob / File Input in the Browser

parseOffice and OfficeConverter.convert accept a web Blob/File (or any BlobLike with an arrayBuffer() method), so browser callers no longer convert to a Buffer first. A filename drives extension-based detection; a nameless blob resolves through magic-byte sniffing.

const file = document.querySelector('input[type=file]').files[0];
const ast = await parseOffice(file);   // Blob/File accepted directly

4. Opt-In Inline Formatting and Iframe Preservation

  • MdGeneratorConfig.fallbackToHtml.inlineFormatting (default false, opt-in even when fallbackToHtml is true) round-trips inline colour, highlight and font size through .md as a sanitized <span style> run — formatting that has no Markdown syntax and was otherwise lost when .md is the storage format.
  • HtmlParserConfig.preserveIframes (default false) keeps non-YouTube <iframe> embeds that are otherwise dropped. true preserves any iframe; an array is a hostname allowlist. The src is scheme-checked on generation, so a javascript:/data: src never survives.

🔧 Also Fixed

  • Chunking dropped HTML- and Markdown-origin paragraph text. ChunkingGenerator read a node's own .text with no fallback to its children, so paragraphs built as { children: [...] } chunked to empty. It now collects text recursively, restoring near-parity with .to('text').
  • <pre><code> blocks did not decode HTML entities. Escaped characters (&lt;, &gt;, &amp; — e.g. a mermaid --&gt; arrow) surfaced still-escaped in text/Markdown/chunk output and double-escaped a little more each HTML round trip. They are now decoded to their literal characters.
  • npm test now runs end-to-end on Windows. The build/test scripts use Node's fs instead of mkdir -p/cp/rm -rf, the ESM test helper loads the bundle via a file:// URL, and the CLI test launches the CLI through node rather than the npx shim (which a direct spawnSync cannot start on Windows). Test-tooling only — the published library is unchanged. (#111)
  • The public config and *Metadata types are exported from the package root, so import type { HtmlGeneratorConfig } from 'officeparser' resolves (previously only the browser .d.ts carried them).
  • rtfConfig was the one generator sub-config not deep-merged in config resolution; the merge is corrected so future fields behave like every other sub-config.
  • Documentation: generate(ast, 'chunks') returns an OfficeChunk[] array, not a JSON string; consumers serialize to JSON/JSONL themselves.

🛠 Getting Started

npm install officeparser@7.6.1

🔗 Full Changelog: View v7.6.1 details
🔗 Documentation & Visualizer: officeparser.harshankur.com