Repository navigation
v8.2.0: 🧩 Template Conditions, Loops & Formatting, Fonts in the Native PDF Engine
I am pleased to announce the release of officeParser v8.2.0! Word templates grow from fill-in fields into real documents: conditions, loops over paragraphs and table rows, and Word's own number and date formats. The native PDF engine now draws in fonts you give it, so any script converts, and writes a bookmark outline. PDF parsing can keep to the headings a document tags, Excel date and time cells read as the sheet shows them, and allowRemoteAssets: false makes sure a parse fetches nothing from a CDN.
Thanks to @MasaGon and @L4XB, whose reports and fixes drove this release.
A huge shoutout to Raymond Camden (@cfjedimaster), whose ideas shaped this release's template work: conditions, loops and Word's number and date formats grew out of his suggestions.
Warning
Behavior changes
- Excel date and time cells are text. A cell formatted as a date, a time or a duration reads as ISO text (
2024-09-30,14:05:00,36:00:00), where it read as the number Excel stores (45565,0.25,1.5). If you converted serial numbers yourself, drop that step. - Templates read tags they left as typed before.
{{ if paid }}and{{ for line in lines }}are now blocks, so one without its closing tag rejects withTEMPLATE_SYNTAX, and{{ name | upper }}or{{ total | format "0.00" }}is now filled.{{else}},{{endif}}and{{endfor}}are block tags: a field with one of those names is no longer filled, and one with no block to belong to rejects.{{if}}and{{for}}alone, which were filled as fields namedifandfor, are now left as text. A block keyword spelt as other template languages spell it (elseif,elif,end if) rejects, naming the spelling to use. With custom delimiters made of letters or digits, a tag is the shortest stretch between a start and an end delimiter. - A template is held to
decompressionLimits.maxXmlElements, as a parse is: a template whose elements, tags and the tokens in its tags together number more than that rejects withXML_ELEMENT_LIMIT_EXCEEDED. - A template field whose value is a list, an object or a function rejects with
TEMPLATE_DATA_INVALID, where it was written as[object Object], its items joined by commas, or the function's source. Loop over a list, or name the field you want ({{ customer.name }}). - A Markdown table's header row is a header, and no longer bold. It is written as
<th>in a<thead>, and in DOCX, ODT, RTF and LaTeX as a repeating header row without added bold, as a header row from HTML or Word already was. Bold an author typed in a header cell is kept. RTF now writes every header row with\trhdrand reads a\trhdrrow as the header. - HTML output loads Chart.js only for a page that draws a chart. With no chart script to draw with (
htmlConfig.chartJsSrc: '', or a parse underallowRemoteAssets: false), a chart is written as a table of its values, with aCONTENT_NOT_REPRESENTABLEwarning. - The native PDF engine's
CONTENT_NOT_REPRESENTABLEwarnings carrydetails.reason, and the one for text that could not be drawn comes once, when the conversion ends, with a count and a sample. A chart raises it too. - An embed size or video id that is not one is not read. A Markdown or HTML width or height must be a CSS length, and a video id an id; a
::youtubedirective with an id that is not one stays text.
🌟 Key Highlights
1. Template Conditions and Loops
OfficeTemplate.render now keeps or drops content with {{ if }} and repeats it with {{ for }}. Where the tags sit decides what they cover, so a template needs no special markup:
| Where the tags are | What repeats or is kept |
|---|---|
| In one paragraph | The text between them |
| In paragraphs of their own | The paragraphs between them (the tag paragraphs go too, leaving no blank line) |
| In different cells of one table | Whole table rows |
{{ for line in lines }} {{ line.item }} {{ line.qty }} {{ line.price | format "$#,##0.00" }} {{ endfor }}
{{ if total > 1000 }}Free shipping.{{ else }}Shipping: $15.{{ endif }}
Conditions compare and combine fields, numbers, strings, dates, true, false and null. Inside a loop, loop.index, loop.number, loop.first, loop.last and loop.length describe the pass. Each copy of a picture, bookmark, paragraph, content control, footnote or comment gets ids of its own, so the output opens in Word without repair, and a footnote in a loop reads that pass's item. Two new errors name the tag and the part when something does not fit: TEMPLATE_SYNTAX and TEMPLATE_DATA_INVALID. decompressionLimits.maxRepeatedContent bounds what loops can add.
2. Word's Number and Date Formats
{{ total | format "$#,##0.00" }} -> $1,250.50
{{ due | format "MMMM d, yyyy" }} -> November 1, 2026
{{ name | upper }} -> ACME CORP
The pictures are Word's own (its \# and \@ switches), with sections for negative numbers and zero, percentages and quoted text. Formatting is done by officeParser, not the runtime's Intl, so a template renders the same in every Node version and browser. numberSeparators, dateNames and utcOffsetMinutes set what it writes, and formatters adds filters of your own.
3. Fonts and Outlines in the Native PDF Engine
pdfConfig.fonts and pdfConfig.fallbackFonts take .ttf/.otf files. Each character is drawn in the first font that has it, words are shaped (ligatures, Arabic joining, Indic conjuncts), and a right-to-left line is laid out from the right edge. A Greek, Hindi, Arabic or CJK document no longer converts to question marks. Fonts are subset to the glyphs used. Embedding needs the optional peer @pdf-lib/fontkit; without fonts nothing changes. pdfConfig.outline: true now writes a bookmark outline in the native engine too.
4. PDF Headings from Tags Only (#117)
pdfParserConfig.headingDetection: 'tags' gives only the headings a PDF's structure tree tags, and HeadingMetadata.levelSource says what decided each heading ('tags' or 'font-size').
5. Excel Dates and Times (#115)
A cell whose number format shows a date, a time or a duration reads as the sheet shows it, in ISO form, in 1900 and 1904 workbooks alike, as ODS cells already read. Contributed by @L4XB in #116.
6. No Request You Did Not Ask For
With allowRemoteAssets: false, a parse that would load the pdf.js worker or Tesseract's files from a CDN rejects with the new REMOTE_ASSET_BLOCKED error, naming the option to set. ocrConfig.cacheMethod controls Tesseract's language-data cache, and maxPasswordAttempts sets how often onPassword is asked.
🔧 Also Fixed
- Markdown tables keep a line break alone in a cell, and quoted directive and attribute values (
::youtube{id="..."},{width="50%"}) lose their quotes. - OCR runs each image on a worker created with that parse's options, not one another parse created with other paths.
- A config naming some
ocrConfigoptions gets the defaults for the rest: one giving bothlanguageandworkerPathwas taken as complete, its other options left unset and unchecked. - An invalid
headingDetectionvalue is reported withINVALID_CONFIG_VALUE, and the default is used. OfficeTemplate.renderreports a misspelt option, and anonMissingvalue it does not take, using'keep'.- A
decompressionLimitsvalue that is not a number of 0 or more is reported, and its default used: one such asNaNor'x'used to turn its bound off without a word. - HTML under hundreds of nested wrappers is read in linear time.
- The native PDF engine wraps header cells at their bold width, and draws em spaces, thin spaces and other spaces as spaces.
🛠 Getting Started
npm install officeparser@8.2.0🔗 Full Changelog: View v8.2.0 details
🔗 Documentation & Visualizer: officeparser.harshankur.com