harshankur/officeParser
 Watch   
 Star   
 Fork   
7 days ago
officeParser

v7.8.0

v7.8.0: 📺 Standard Markdown for Embeds, Safe Iframe Capture, and a Cleaner HTML Round Trip

I am pleased to announce the release of officeParser v7.8.0! Embeds were the one construct in the Markdown dialect with no borrowed convention and the worst degrade: a YouTube video could only be written as an invented <div data-youtube-video> block that renders as an invisible empty box on GitHub, and a raw <iframe> was escaped into a wall of literal text. This release gives embeds a real, selectable Markdown form, adds a safe path for capturing untrusted iframes, and fixes an HTML round-trip whitespace bug.

Everything here follows the same rule as recent releases: no regression on any consumer. Every change is additive, a genuine bug fix, or non-standard becoming standard; where a default would move, the old behavior stays and is deprecated. The default embed output is byte-identical to 7.7.0.


✨ What's New

1. A Markdown form for embeds, selected by mdConfig.dialect.embeds

Choose how an embed node is written:

  • 'html' (default): the <div data-youtube-video> / <iframe> single-line block this library has always emitted and re-reads.
  • 'directive': a remark-directive leaf, ::youtube[Label]{id=… width=… align=…} / ::embed[Label]{src=… …}, both parsed and generated. An editor round-trip form (GitHub renders it verbatim rather than as a player, so it is not a GitHub-interop format).
  • 'link': a plain [YouTube](url) / [Embed](url).
  • 'thumbnail': a YouTube-only clickable preview [![Label](…/vi/ID/…)](watch), the best GitHub degrade.

::youtube parses unconditionally (rendered from a validated id via a fixed template). ::embed carries an arbitrary src, so it is gated behind preserveIframes (the trust input) and stays literal text otherwise. Unknown ::names stay literal, with no catch-all.

2. Safe capture of untrusted iframes: htmlConfig.gatedEmbeds

Off by default. When on, a generic (non-YouTube) iframe embed is emitted as an inert <div data-embed-gated data-embed-src> placeholder that never auto-loads its src. An editor renders a click-to-load control from it, and HtmlParser reads it back to the same embed node. The src is scheme-checked on emit. The default output (a live <iframe>) is unchanged. Combined with the existing preserveIframes gate, untrusted input is never escaped-as-text and never auto-rendered.

3. Opt-in import of ambiguous "folk" forms: htmlParserConfig.embedFolkForms

Off by default. When on, a standalone Obsidian image whose URL is a YouTube link (![](…watch?v=ID)) and a clickable thumbnail-link ([![](…/vi/ID/…)](watch)) import as safe YouTube embeds. Off by default because auto-upgrading an image or link is a heuristic that could mangle a genuinely-intended image link. The unambiguous forms are always recognized regardless of this flag.

4. EmbedMetadata.label

The human label of a ::youtube[Label] / ::embed[Label] directive (and a gated embed's caption). It round-trips through the directive form, the generic gated data-embed-label, and the YouTube editor-HTML shape.


⚠️ Deprecated

fallbackToHtml.embeds (boolean). Use mdConfig.dialect.embeds instead, which also selects the 'directive' and 'thumbnail' forms. While dialect.embeds is unset the boolean is still honored (true maps to 'html', false to 'link'). It will be removed in the next major.


🔧 What's Fixed

1. Markdown and HTML now parse a YouTube iframe the same way

The Markdown parser read a YouTube <iframe> as a generic 'iframe' embed with no videoId, and only under preserveIframes, while the HTML parser read it as 'youtube' unconditionally. Both parsers now detect a YouTube src the same way, before the preserveIframes gate, so the same input yields the same 'youtube' embed. The youtube-via-iframe HTML path now also carries the iframe's width/height.

2. An inline link was fenced by blank lines on md → HTML

HtmlGenerator appended a readability blank line after every node, including inline text and link runs, so See this [video](url). emitted a paragraph with \n\n around the <a>, which reparsed as a stray space before the punctuation. The blank line is now added only after block-level nodes; inline runs concatenate directly. This is a whitespace-only change to generated HTML (semantically identical), and it makes the md → HTML → md round trip correct.


🛠 Getting Started

npm install officeparser@7.8.0

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

8 days ago
officeParser

v7.7.0

v7.7.0: 🎯 Round-Trip Fidelity for Rich-Text Editors + Syntax-Typed Markdown Dialects

I am pleased to announce the release of officeParser v7.7.0! This release hardens the Markdown ↔ HTML round trip for the kind of rich content a ProseMirror/Tiptap-style editor produces. Nested lists, aligned tables, highlights, and link/image titles now survive a full md → HTML → md cycle. It also reworks the Markdown dialect configuration so every capability is named by the syntax it selects instead of a product flavor or a bare boolean.

The fidelity items are corrections: the only output that moves is content that was previously flattened, dropped, or emitted as invalid markup. The dialect rework is backward-compatible: existing boolean/flavor configs still work (they coerce to the new values), and are now deprecated.


✨ What's New

1. Dialect capabilities are typed by syntax, not flavor

Each mdConfig.dialect capability now names the syntax it selects: admonitions: 'blockquote' | 'fence' | 'fence-attribute', and strikethrough/definitionLists/footnotes/citations/wikilinks/attributeLists as '<marker>' | 'none' (e.g. strikethrough: 'tilde', wikilinks: 'double-bracket'). A shared convention is a single value, and a second syntax can be added later without a breaking change. A new highlight: 'equals' | 'none' capability joins them.

2. ==highlight== round-trips through Markdown

In dialects that define it (Obsidian/extended), a highlighted run emits as ==text== and ==text== parses back to a highlight. Other dialects keep the HTML <mark> fallback.

3. Link & image titles are preserved

[text](url "Title") and ![alt](img.png "Title") now keep their title in both directions. Previously an inline destination swallowed url "Title" as one URL.

4. fallbackToHtml.itemLineBreaks

A multi-paragraph list item joins onto its single Markdown line with <br> (default on), mirroring cellLineBreaks for table cells.


🔧 What's Fixed

1. Nested lists survive the round trip

An HTML <li> that wraps its text in <p> (the shape rich-text editors emit) exported as - a\n\n\n - a1, which reparsed flat. List items are now tight, a conservative parser pass rejoins a blank-line-split child, and generated HTML nests spec-validly (<li>a<ul>…</ul></li> instead of the invalid sibling shape), which also makes generated EPUB XHTML valid.

2. GFM table column alignment through HTML

:--- / :---: / ---: alignment now lives on each cell and is emitted as text-align on <th>/<td> (and read back), so per-column alignment survives md → HTML → md, the editor import path, instead of vanishing on the HTML hop.

3. HTML inline & block fidelity

Inline code emits <code> (not a font-family: monospace <span>); a single-line code block with a language stays a <pre><code> block; a plain <blockquote> round-trips to > quoted; an HTML <br> reads back as a hard line break rather than collapsing to a space; and an own-line $$…$$ parses as block math instead of leaking stray $.


⚠️ Deprecated

Boolean dialect toggles (strikethrough: true) and admonition flavor names (admonitions: 'github') still work. They coerce to the new syntax values (true becomes the marker, false becomes 'none'; flavors become 'blockquote'/'fence'/'fence-attribute'), but are deprecated and will be removed in the next major. Prefer the syntax names.


📝 Also

  • Fixed a cold-run npm test flake where a PDF-OCR parity timeout was killed at the 30s cap and misreported as a 0.0% similarity content mismatch. Timeouts now surface distinctly and the OCR run gets a 120s budget. Test tooling only. (#111)

🛠 Getting Started

npm install officeparser@7.7.0

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

10 days ago
officeParser

v7.6.2

v7.6.2: 🔁 Lossless Markdown Round-Trips for Code Blocks, Inline Code, Line Breaks & Tables

I am pleased to announce the release of officeParser v7.6.2! This patch closes four Markdown/HTML round-trip fidelity bugs found while driving a real editor's save → load → save cycle through officeParser: a single-line code block losing its language, inline code losing its backticks, a raw <br> being escaped, and a table header emitting invalid HTML. Each was a case where officeParser's own output did not reparse into what produced it; all four now round-trip cleanly.

These are corrections, not new behavior to guard against: the only output that moves is content that was previously lost, escaped, or emitted as invalid markup, so upgrading fixes those cases rather than disturbing working ones. If you built a workaround for any of them, you can drop it.


🔧 What's Fixed

1. Single-line code blocks keep their language

MarkdownGenerator chose fenced-vs-inline purely by whether the code text contained a newline, ignoring the language, so a one-line code block (const x = 1; tagged js) or a one-line mermaid diagram collapsed to an inline `code` span. That silently dropped both the language and its block-ness, and re-imported as an inline code mark rather than a code block. A code node is always block-level (genuinely inline code is a monospace text node), so a code node with a language now always emits as a fenced block regardless of newlines. A tagged `const x = 1;` becomes a proper js block again.

2. Inline code keeps its backticks

Inline code parses to a monospace text node, but the generator's text-node path had no backtick emission for it, so every inline `code` (and inline <code> from HTML) degraded to plain text on md → md and html → md. Monospace text is now re-wrapped in backticks, fence-sized so an embedded backtick can't close the span early, with emphasis wrapping the span so **`code`** round-trips.

3. Raw <br> round-trips symmetrically

MarkdownGenerator emits a raw <br> for a line break inside a table cell (a GFM pipe cell cannot hold a newline), but MarkdownParser did not read it back. It escaped <br> to &lt;br&gt;, destroying the break on the md → html hop, in cells and paragraphs alike. The parser now recognises <br> / <br/> / <br /> as a hard line break, symmetric with what the generator writes, so a <br> survives the round trip. A table-cell line break, ubiquitous in Word-imported forms and exams, is now preserved.

4. Valid, self-idempotent table headers

generate('html') emitted a table's header cells directly under <thead> (<thead><th>…) with no wrapping <tr>, which is invalid HTML that officeParser's own HtmlParser could not read back as a table, so a md → HTML → md round trip lost the header (its cells came back empty). The header row is now wrapped in a <tr> (<thead><tr><th>…), which is valid and self-idempotent.


📝 Also

  • Heading anchors are already toggleable. generate('md') appends a kramdown/Pandoc {#slug} suffix to headings (# Title {#title}), which GFM/CommonMark render as literal text. The existing top-level generateIds: false omits it (and the HTML heading ids). It is a generator-wide option, not under mdConfig, which made it easy to miss. Now documented in the README.

🛠 Getting Started

npm install officeparser@7.6.2

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

11 days ago
officeParser

v7.6.1

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

26 days ago
officeParser

v7.5.1

v7.5.1: 🛡️ Corrupt Files Fail Loudly, Real-World Archives Detect Correctly & Webpack-Safe Bundles

I am pleased to announce the release of officeParser v7.5.1! This patch release closes three reported issues around one theme: trusting the file in front of us less, and telling you more. Unreadable input now fails with typed errors instead of parsing as an empty document, type detection reads the archive's own declaration when magic-byte sniffing gives up, and the browser bundles no longer break webpack builds.

Thanks to @benhid, @SteveNewhouse and @ayazaurie, whose reports drove this release.

[!WARNING] Behavior changes

  • Corrupt input throws; it no longer parses as an empty document. Data that is not a ZIP archive, an archive cut off in transfer (or carrying data after its end-of-archive record), and a readable ZIP missing the part its format requires (word/document.xml, xl/workbook.xml, ppt/presentation.xml, ODF content.xml, the EPUB OPF) now reject with ZIP_NO_ENTRIES_FOUND, ZIP_TRUNCATED and REQUIRED_PART_MISSING respectively. An empty result therefore means the document really is empty. If you relied on unreadable files resolving quietly, catch and branch on error.officeIssue.code.
  • DOCX headers, footers and comments now appear in output. They were documented but never extracted, so ast.auxiliary.headers / .footers were always empty and comments never reached the AST. Documents that have them will now produce them; ignoreHeadersAndFooters and ignoreComments restore the old shape if you want it.
  • Config objects are no longer shared with the library. Reusing one config across calls previously leaked each parse's warnings into earlier, already-returned ast.warnings arrays, and generation could silently rewrite htmlConfig.containerWidth on your own object. Resolution now copies; callbacks and abortSignal keep their identity.

🌟 Key Highlights

1. Corrupt, Truncated and Mislabeled Files Fail Loudly (#107)

Since the 7.3.0 streaming ZIP rewrite, a corrupt buffer resolved into an empty AST with no warnings, indistinguishable from a genuinely empty document. Three distinct failure modes are now typed rejections, and every thrown error carries its structured issue so you branch on a stable code instead of matching message text:

try {
    const ast = await parseOffice(buffer, { fileType: 'docx' });
} catch (err) {
    switch (err.officeIssue?.code) {
        case 'ZIP_NO_ENTRIES_FOUND':  // not a ZIP archive at all
        case 'ZIP_TRUNCATED':         // cut off in transfer, entries incomplete
        case 'REQUIRED_PART_MISSING': // readable ZIP, but not the format it claims
            console.error('Unusable file:', err.officeIssue.message);
            break;
        default:
            throw err;
    }
}

The OfficeError type is exported for TypeScript users. Files that are legitimately empty still parse and now say so: a chartsheet-only workbook emits NO_WORKSHEETS_FOUND and a zero-slide deck emits NO_SLIDES_FOUND through onWarning / ast.warnings, so an empty result is never silent in either direction. Two ODF gaps closed alongside: a valid .ods/.odp without a mimetype entry was walked as a text document and came back empty (it now falls back to your fileType or the extension), and a missing root content.xml can no longer silently promote an embedded Object N/content.xml chart to the document body.

2. Type Detection Reads the Archive's Own Declaration (#82)

Passing a bare Buffer relied entirely on magic-byte sniffing, which walks a ZIP under fixed budgets: at most 1024 entries, and roughly 1 MiB of scanning when entry sizes are deferred to trailing data descriptors (general-purpose flag bit 3, what streaming ZIP writers produce). A valid PPTX whose [Content_Types].xml sat beyond either budget was reported as generic zip, so parseOffice threw "Sorry, OfficeParser currently supports docx, pptx, xlsx... add support for zip files" for a perfectly good document. Both layouts occur in the wild, and every ZIP-backed format was affected, in Node and in the browser.

When sniffing is inconclusive, the archive is now opened with officeParser's own reader, which has neither budget, and the format is taken from [Content_Types].xml or the ODF/EPUB mimetype entry. Detection inflates at most 4 MiB regardless of your decompression limits, a correct fileType hint skips the archive scan entirely and remains the fastest path, and a bare zip sniff is no longer misreported as a BUFFER_TYPE_MISMATCH against your own hint.

3. Webpack-Compatible Browser Bundles (#108)

One dynamic import inside the bundles escaped the build's webpackIgnore annotations, because the annotator treated an interpolated template literal as a static string. Webpack does not skip a specifier it cannot resolve: it builds a context module over the whole directory, so consumers ended up bundling all of dist/, Node-only files included, and their builds failed on child_process. The annotator was rewritten around a shared, tested classifier, child_process and url now resolve to browser stubs, and a webpack 5 build of the ESM and slim ESM bundles is warning-free. Shipping checks now scan every bundle so this class of regression cannot ship again.


🔧 Also Fixed

  • DOCX headers, footers and comments were never extracted. The parse code existed but the parts were missing from the archive extraction filter, so it could never run. Part names now also match past header9.xml, which documents with several sections reach.
  • PPTX document-property parts were parsed as slide candidates. docProps/app.xml and custom.xml are now skipped in the slide loop on identity, closing the same phantom-slide trap that ppt/presentation.xml needed.
  • Typed errors were reported twice, with a doubled [OfficeParser]: prefix and their code flattened to FILE_CORRUPTED by the wrapping layer. Errors now pass through once, intact.
  • Archive extraction errors ignored outputErrorToConsole and onWarning, always writing to the console. They now report through your handlers like every other issue.
  • Memory: the Word, Excel and PowerPoint parsers each kept the full XML source of every extracted part in a write-only buffer under includeRawContent. Dropped; serialized ASTs are byte-identical.

🛠 Getting Started

npm install officeparser@7.5.1

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

2026-07-27 05:47:05
officeParser

v7.5.0

v7.5.0: 🧮 Equations From Every Format & ODF Page Breaks

officeParser v7.5.0 makes equations a first-class part of the AST across every format that can carry them, and closes two ODF gaps: paragraph styling that never reached the text, and page breaks that were never emitted.

Thanks to @njoqi and @benhid, whose reports and sample documents drove this release.

[!WARNING] Behavior changes

  • Equations are now code nodes, not text. ODF equations previously came through as plain text nodes holding an ad hoc notation ((1)/(2)). They are now code nodes carrying CodeMetadata.math, with LaTeX in node.text (\frac{1}{2}) — matching what Markdown already produced. ast.toText() output changes accordingly.
  • Redundant heading emphasis is no longer emitted. A heading whose every run is bold used to render as # **Heading** in Markdown, and in RTF/HTML as an inner font size that overrode the heading's own. Uniform bold/size inside a heading or table header row is now dropped in favour of the element's own styling. Partial emphasis (# Normal **Bold** Normal) is untouched.
  • metadata.styleMap flags are tri-state. A style that explicitly turns formatting off (ODF's fo:font-weight="normal") now appears as false rather than being absent. On styleMap, absent means the style is silent and the property inherits; false means it is explicitly off. Code resolving inheritance itself must test === undefined, not truthiness. Content nodes never carry false.

🌟 What's New

1. Equations, From Every Format, Normalized to LaTeX (#97)

Equations were not merely missing before this release — they were silently corrupted. Office documents ship them in two markups: OOXML's <m:oMath> (DOCX, PPTX) and MathML <math> (ODF embedded objects, HTML, EPUB3). Neither was handled, so both fell through to a generic "concatenate the descendant text" fallback:

Before Now
½ + ⅛ + ¹⁄₃₂ 12+ 18+132 \frac12+ \frac18+\frac1{32}
3/42 342 \frac3{42}
(2³×2⁷)² (23×27)2 {(2^3×2^7)}^2
f(x) = ⅓x³ + … fx= 13x3+… f(x)= \frac13x^3+…
R \mathbb{R}

1/2 → 12 and 3/42 → 342 produce plausible-looking numbers, so nothing downstream could tell the value was wrong. In one reporter's maths exam paper, 37 of 121 equations were affected. PowerPoint was worse in a quieter way: its run loop dispatches on <a:r>, and an <m:oMath> is a sibling of the runs rather than one of them, so PPTX equations were dropped entirely.

Every format now converges on one node:

Code Node (type: 'code')
├── text: '\frac{1}{2}'          // LaTeX, whatever the source markup was
└── metadata: { math: 'inline' | 'block' }

Fractions, sub/superscripts, radicals, delimiters, n-ary operators, named functions, accents, bars, matrices and math alphabets are all preserved. A document's own <annotation encoding="application/x-tex"> is used verbatim in preference to anything reconstructed from the presentation markup. Because everything is LaTeX, a formula now survives a docx → md → docx round trip instead of degrading at each hop.

2. includeBreakNodes Now Works for ODF (#104)

It was implemented only in WordParser, and the docs said "DOCX only". The reason was that ODF has no inline break element to find — page and column breaks live on the paragraph style as fo:break-before / fo:break-after. Those now emit break nodes, and <text:soft-page-break/> maps onto the same lastRenderedPage type DOCX uses.

[!NOTE] DOCX writes breaks inline, so they land as children of the paragraph. ODF scopes them to the paragraph style, so they are emitted as siblings around it.

3. ODF Paragraph Styles Reach the Text (#104)

A paragraph style carrying fo:font-weight, fo:font-size or fo:color was parsed into the style table and referenced from the node's metadata — but the runs inside were built with empty formatting, so every generator emitted the text unstyled. The formatting sat visible in the AST and was simply never applied.

Runs now inherit the paragraph's formatting. That made a second gap load-bearing: styles that explicitly turn formatting off were not recorded at all. LibreOffice writes fo:font-weight="normal" whenever you un-bold part of a bold-styled paragraph, and with nothing recorded, that span had no way to override what it now inherited. Off-states are recorded, and a span carrying one clears the inherited value.


🔧 Also Fixed

  • Every table subtree was rendered twice (#105). The Markdown generator walked a node's children to build its output, then the table processor discarded that and traversed the rows and cells again. The two traversals compounded with nesting depth, so conversion time grew quadratically — a 184 KB document of nested tables took 7.5s. Footnotes inside table cells were collected on both passes and appeared twice. Time now grows linearly with input size.
  • Text inside PowerPoint grouped shapes was silently dropped (#106). traverseSpTree looked for a nested <p:spTree> inside each <p:grpSp>. A group does not contain one — per the schema it has the same content model as spTree itself — so the lookup always failed and the group's contents were skipped. Nested groups are covered too.
  • Security: an ODF table row carrying a large table:number-rows-repeated with no cells bypassed the per-document cell budget entirely, so a 733-byte file could exhaust memory and crash Node. Zero-cell rows are now charged against the same budget as every other repeat.

🛠 Getting Started

npm install officeparser@7.5.0

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

2026-07-20 05:48:22
officeParser

v7.4.0

v7.4.0: 📝 Markdown Output Dialects, Metadata Overrides & Robust Validation

I am thrilled to announce the release of officeParser v7.4.0! This release brings major enhancements to our Markdown generation capabilities with targeted dialects, introduces metadata overrides for document authors, enables granular HTML attribute pass-through, and applies another critical layer of validation and security hardening across spreadsheet parsing and recursive nesting.

[!WARNING] Behavior changes in this release:

  • RTF hyperlink scheme restriction: RTF hyperlinks are now restricted to the same safe schemes allowed in HTML and Markdown. Local intranet file:// or UNC path links will lose their click targets (the link text is kept).
  • styleMap default mappings in HTML: styleMap's output.tag now takes effect in HTML output. This activates built-in default style mappings for HTML (e.g. mapping Heading N/Quote/Title-styled paragraphs to their respective semantic tags).
  • Clamped repeated spreadsheet cells: ODF spreadsheets with table:number-columns-repeated or table:number-rows-repeated are now capped at decompressionLimits.maxTableCells (default: 1,000,000) to prevent memory exhaustion from zip bomb constructs.
  • Lowered HTML nesting-depth guard: Lowered the HTML parser's nesting-depth guard to 256 (from 1000) so a deeply nested document raises the typed error instead of a range error.

🌟 Key Pillars of the v7.4.0 Update

1. Markdown Output Dialects & HTML Fallbacks

I've significantly upgraded the Markdown generator to support real-world flavors:

  • Targeted Dialects (MdGeneratorConfig.dialect): Generate output optimized for specific platforms: 'github', 'gitlab', 'obsidian', 'pandoc', strict 'commonmark', or the default 'extended'. You can also pass a detailed MarkdownDialectConfig object to toggle admonitions, footnotes, citations, wikilinks, math, tables, list markers, and emphasis on a per-feature basis.
  • Granular HTML Fallbacks (MdGeneratorConfig.fallbackToHtml): The fallback flag can now accept a FallbackToHtmlConfig object to independently control how text formatting, alignment, anchors, tables, embeds, and cell line breaks degrade to HTML when not natively representable. The plain boolean form remains supported.

2. Metadata Overrides & Document Reproducibility

  • Metadata Overrides (GeneratorConfig.metadataOverrides): Author metadata can now be specified at the generation stage (including title, author, subject, keywords, created, modified, language, and custom properties) without mutating the parsed AST. This applies across HTML, EPUB, Markdown, RTF, and plain text/CSV headers.
  • EPUB Reproducibility: Specifying the modified override metadata yields byte-stable EPUB output by fixing the dcterms:modified property and all zip entry modification times (which previously defaulted to the current runtime).
  • Keywords and Subject: These properties are now written out to HTML <meta>, Markdown frontmatter, EPUB dc:subject, and RTF \info.

3. HTML Attribute Pass-through

  • Preserve Attributes (htmlParserConfig.preserveAttributes, default false): You can now opt-in to preserve generic HTML attributes on round-trips. They will map to BaseContentNode.htmlAttributes. High-risk attributes like event handlers (on*), srcdoc, inline style, and id are filtered out.

4. Expanded Markdown/Text Parsing

  • Parsers have been expanded to handle reference-style links/images, backslash escapes, underscore emphasis, multi-backtick inline code, setext headings, <url> autolinks, HTML entity and character references, and ~~~-fenced blocks.
  • Footnotes now degrade gracefully when disabled or under strict CommonMark, rendering as parentheticals rather than disappearing.

5. Security & DoS Hardening

  • Spreadsheet Cell Cap: Clamps pathologically large repetition counts in ODF spreadsheets, preventing memory exhaustion.
  • Recursive Nesting Guard: The HTML parser's nesting guard has been lowered to 256 (from 1000) to safely throw a typed error before Node.js stacks overflow.
  • Config pollution protection: Standardized deep config merges now explicitly guard against __proto__ pollution during JSON parsing.
  • CSS sanitization bypass fixed: sanitizeCssValue now strips backslash escapes before checking for prohibited patterns like url().
  • RTF Hyperlink schema restrictions: hyperlinked paths are restricted to safe web schemes.
  • Contextual escaping: Fixed missing escapes in Markdown generation for math blocks, wikilinks, citation keys, admonition types, and footnote IDs.

🔧 Also in This Release

  • Fixed: Plain-text .to('text') and .to('md') output no longer trims document-level whitespace, resolving issue #102 by only stripping the generator's internal separator artifacts.
  • Fixed: .to('text') now preserves chart data series, CSV comments, and separates adjacent table cells cleanly instead of merging them.
  • Fixed: CSV formula safety prefixing is now tested against trimmed cell values.
  • Fixed: Inline stylesheet parsing is now powered by a real CSS declaration parser rather than substring matching.

🛠 Getting Started

npm install officeparser@7.4.0

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

2026-07-13 04:48:30
officeParser

v7.3.0

v7.3.0: 📚 EPUB Support, an Extended Markdown Dialect & Security Hardening

I am thrilled to announce the release of officeParser v7.3.0! This release adds EPUB as a first-class parsed and generated format, brings the Markdown/HTML pipeline up to parity with the rest of the ecosystem (task lists, admonitions, footnotes, citations, wikilinks, and more), and closes out a full security-hardening pass across the engine.

[!WARNING] Behavior change: standalone: false HtmlGeneratorConfig.standalone (plain boolean false) now emits a genuinely bare HTML fragment — no <style>, no <script> — instead of the old global, unscoped stylesheet that would leak onto whatever page you embedded it in. If you relied on that stylesheet, use standalone: { document: false } instead, which reproduces the old output byte-for-byte.


🌟 Key Pillars of the v7.3.0 Update

1. EPUB Support (Parser & Generator)

epub is now a first-class format on both sides.

  • Parsing: EpubParser unzips the archive, resolves the spine's reading order, and parses each XHTML document through the existing HtmlParser — so EPUB content gets the same AST shape (and the same Markdown-dialect fidelity below) as every other format. Dublin Core metadata (title, author, description, language, etc.) maps straight into ast.metadata.
  • Generating: EpubGenerator produces a valid, strict-XHTML EPUB 3 archive, with images packaged as real zip entries (not data: URIs, which most e-readers won't render).
  • ⚠️ Pass extractAttachments: true when converting to/from EPUB if the document has images — OfficeConverter.convert() already does this for you.

2. A Much Richer Markdown Dialect

The following now round-trip through both the Markdown and HTML pipelines:

  • GFM task lists- [x] Done / - [ ] Todo
  • Admonitions/alerts — GitHub's > [!NOTE] and GitLab's :::note ... :::
  • Real footnotes[^id] references and definitions (previously faked as blockquotes)
  • Definition lists & abbreviations — Markdown Extra style
  • Pandoc attribute lists{width=50% .centered} on images and tables
  • Citations[@citekey]
  • Wikilinks — Obsidian-style [[Page]] / [[Page|Alias]]
  • Math — inline $...$ and block $$...$$ LaTeX now tokenise instead of passing through as plain text
  • MDX import stripping — JSX tags are stripped on import (parse-only; never authored back out)
  • Frontmatter arraystags: [a, b] now parses to a real array, not a literal string

Plus long-standing HTML round-trip gaps closed: image size/alignment, table alignment, merged cells (colspan/rowspan), and YouTube embeds all now survive a save → reload cycle instead of being write-only.

3. Granular HTML Envelope Control

HtmlGeneratorConfig.standalone now accepts an object, not just a boolean:

htmlConfig: {
  standalone: {
    document: false,      // drop the <html>/<head>/<body> shell
    styles: 'scoped',     // new: CSS wrapped in @scope so it can't leak onto a host page
    // metaTags, scripts, headInjections, bodyInjections all independently toggleable
  }
}

Every field defaults to its "on" (fully-standalone) value when omitted, so { document: false } alone gives you a fully-styled fragment with just the outer shell removed. See the breaking-change note above for what moved.

4. Security Hardening

A pass across the whole engine treating every parsed document as untrusted input:

  • Centralized output sanitization (src/utils/sanitize.ts) — every generator now escapes document-derived text per destination context: HTML/XML attributes, inline CSS (blocks url()/expression()/javascript:), URLs (rejects script-executing schemes), inline <script> payloads, CSV cells (formula/DDE injection), RTF control words, and Markdown text/URLs.
  • Zip bomb protection — decompression now caps against actual inflated bytes as they stream in, not the ZIP header's (attacker-controlled) declared size.
  • DoS hardening — removed an O(n²) hot path in HTML parsing, capped recursion depth on malicious element nesting, and capped the MDX-unwrap loop's iteration count.
  • SSRF hardening — PDF generation now blocks the rendering browser from fetching any remote resource that isn't an inline image or the configured chart CDN.
  • PDF parsing hardening — disabled pdf.js's eval-based fast path for untrusted PDFs.

All covered by a new dedicated security regression suite (npm run test:security).


🔧 Also in This Release

  • Fixed: Standalone bookmark-anchor blocks and table cells using the alignment-div fallback now survive a Markdown save → reload cycle instead of degrading into escaped literal text.
  • Testing: A new exhaustive fixture suite (npm run test:exhaustive) covering every AST construct per format, run as part of the standard test gate alongside the existing baseline/parity suites.
  • Docs: README and SECURITY.md now spell out officeParser's security posture plainly — I actively harden the library against malicious input (see above), but as with any software, no guarantee of being lapse-free is possible, and responsibility for a compromised input file's downstream impact ultimately rests with the consumer of the library. See the new Security & Trust Boundary section in the README.

🛠 Getting Started

npm install officeparser@7.3.0

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

2026-06-29 04:14:41
officeParser

v7.2.3

v7.2.3: 📦 Slim Browser Bundles, MathML Formula Support, and Dependency Upgrades

I am pleased to announce the release of officeParser v7.2.3! This patch release introduces dedicated slim browser bundles for strict runtime environments, support for formula parsing in ODF, and upgrades core dependency versions.


🌟 Key Highlights

1. Slim Browser Bundles (Manifest V3 Compliance)

To support deployment inside environments with strict security controls (such as Chrome and Edge Manifest V3 extensions where remotely hosted code is prohibited), we now distribute slim ESM and IIFE browser builds:

  • dist/officeparser.browser.slim.mjs (ESM)
  • dist/officeparser.browser.slim.iife.js (IIFE)
  • dist/officeparser.browser.slim.d.ts (Types)

In the slim bundles, the tesseract.js (OCR) engine is completely stubbed out, and default remote CDN references are stripped, reducing bundle footprints and ensuring compliance with web store policies.

2. MathML Formula Support (ODF)

Added native extraction for MathML formulas in OpenOffice/LibreOffice formats (.odt, .odp, .ods) at both block level and inline cell level, preserving mathematical notation.

3. Core Upgrades

  • Upgraded pdfjs-dist to 6.1.200 for optimized rendering performance, modern Node.js compatibility, and security mitigations.
  • Upgraded fflate to 0.8.3 to resolve Zip64 archive parsing issues.

🛠 Getting Started

npm install officeparser@7.2.3

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

2026-06-05 05:10:13
officeParser

v7.2.0

v7.2.0: 🏗️ Parser Enhancements, Granular HTML Generator Controls, and Strict AST Typings

I am thrilled to announce the release of officeParser v7.2.0! This major update brings a massive architectural upgrade to the AST, empowering developers with deeper insight into document layout, embedded metadata, and bulletproof TypeScript integrations.

As we pave the way for building advanced RAG architectures, deep-document search systems, and robust AI parsing pipelines on top of officeParser, v7.2.0 guarantees that every piece of document intelligence—from slide masters to hidden footnotes—is logically structured and heavily typed.

[!WARNING] Soft Breaking Change: Notes Placement
If your application iterates over ast.content to manually extract footnotes, endnotes, or slide speaker notes, you will need to update your logic. These nodes are no longer appended to the main content array. They are now structurally nested inside the notes[] array of their logical parent or preceding text node.


🌟 Key Pillars of the v7.2.0 Update

1. Structural Notes Attachment

Previously, footnotes, endnotes, and slide speaker notes were flattened and appended to the end of the document content. In v7.2.0, these notes are now strictly attached to their logical parent or preceding sibling nodes via a new node.notes[] array. Note: The legacy putNotesAtLast config flag is now deprecated.

2. Auxiliary Content (Headers, Footers, Slide Masters)

The new ast.auxiliary property unlocks out-of-band document templates! officeParser now automatically extracts headers and footers from Word documents (ast.auxiliary.headers / footers), and Slide Masters from PowerPoint presentations (ast.auxiliary.slideMasters). These are neatly separated from the main sequential document flow.

3. Native & Custom Document Properties

The OfficeMetadata interface has been radically upgraded. Alongside canonical metadata fields (title, author, dates), officeParser now exposes format-specific verbatim metadata via ast.metadata.nativeProperties (e.g., <meta> tags in HTML, app.xml stats in DOCX, XMP dicts in PDF) and user-defined variables via ast.metadata.customProperties.

4. Discriminated Unions & Strict AST Typings

The generic OfficeContentNode interface has been completely refactored into a strict TypeScript Discriminated Union. This unlocks precise, compile-time type narrowing per node.type (e.g., safely accessing SlideMetadata only when type === 'slide'), eliminating the need for generic fallback assertions across your application.

5. Interactive HTML Spreadsheet Layouts & DOM Injections

The HTML Generator just got significantly smarter:

  • Interactive Spreadsheets: Spreadsheets generated from Excel or CSV files now render with desktop-class interactivity, featuring native draggable boundary handles (.col-resizer) to dynamically resize rows and columns in the browser.
  • Granular Layout Controls: Expanded HtmlGeneratorConfig with containerWidth, customCss, and DOM injections (head/body hook insertions).

🛠 Getting Started

npm install officeparser@7.2.0

Example of using the new Discriminated Unions, Auxiliary nodes, and Structural Notes:

import { parseOffice } from 'officeparser';

const ast = await parseOffice('presentation.pptx', {
  ignoreSlideMasters: false
});

// Access Slide Masters from the new auxiliary AST branch
const masterSlides = ast.auxiliary?.slideMasters || [];
console.log(`Found ${masterSlides.length} master slides!`);

// Confidently narrow types using Discriminated Unions!
for (const node of ast.content) {
  if (node.type === 'slide') {
    // TypeScript now explicitly knows this is a Slide node.
    // Slide Notes are now structurally nested under the slide!
    const noteCount = node.notes?.length || 0;
    console.log(`Slide ${node.metadata.pageNumber} has ${noteCount} notes attached.`);
  }
}

🔗 Full Changelog: View v7.2.0 Details 🔗 Documentation & Visualizer: officeparser.harshankur.com


❤️ Supporting the Future of Document Infrastructure

Since 2019, officeParser has been maintained as a voluntary project, growing to support over 10 million downloads and 300,000+ weekly installations.

As I build the ultimate document-to-AI pipeline, I seek professional sustainability to fund officeParser's next milestones:

  • Core Sustainability: Keeping up with dependency updates, test coverage, and performance tuning.
  • Multi-Runtime Excellence: Official support for Bun, Deno, and Edge (Cloudflare Workers, Vercel).
  • Enterprise Connectors: Dedicated integrations with LangChain, LlamaIndex, and Haystack.

If officeParser powers your production workflows or AI pipelines, please consider supporting its development:

👉 GitHub Sponsors 👉 Buy Me A Coffee


Changes: v7.1.0..v7.2.0