From 01e085b56114440aa8ca2d172228ba5a343ea5b4 Mon Sep 17 00:00:00 2001 From: David Hagmann Date: Sat, 27 Jun 2026 03:37:13 +0800 Subject: [PATCH] Add `line-numbers` option for Word (docx) output Word stores line numbering in each section's , which pandoc writes from the reference doc after filters run. A filter therefore can't add it without appending a new section, which drops the running head and resets the page size. Instead, register a docx output post-processor that injects into the document's existing sections, preserving each section's header references and page size (Letter, A4, etc.). - format-docx.ts: read `line-numbers` and register the post-processor - document-options.yml: declare the `line-numbers` docx option - changelog entry and a smoke test asserting is present Refs #14624 Co-Authored-By: Claude Opus 4.8 (1M context) --- news/changelog-1.10.md | 4 ++ src/format/docx/format-docx.ts | 65 ++++++++++++++++--- src/resources/schema/document-options.yml | 8 +++ .../2026/06/27/docx-line-numbers.qmd | 17 +++++ 4 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 tests/docs/smoke-all/2026/06/27/docx-line-numbers.qmd diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index 656bc4d65d4..2e4e911def9 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -23,6 +23,10 @@ All changes included in 1.10: - ([#14530](https://github.com/quarto-dev/quarto-cli/pull/14530)): Add `quarto.*` Pandoc template variable namespace. `format.language` is now exposed as `$quarto.language.$` in custom Pandoc templates via the defaults-file `variables:` section, with no leakage into rendered output. +### `docx` + +- ([#14624](https://github.com/quarto-dev/quarto-cli/issues/14624)): Add `line-numbers` option to enable continuous line numbering in Word output. + ### `pdf` - ([#13588](https://github.com/quarto-dev/quarto-cli/issues/13588)): Fix Lua error when rendering PDF with `reference-location: margin` and a footnote alongside a figure with `fig-cap`. (author: @mcanouil) diff --git a/src/format/docx/format-docx.ts b/src/format/docx/format-docx.ts index 00db574aec1..bf3a942c796 100644 --- a/src/format/docx/format-docx.ts +++ b/src/format/docx/format-docx.ts @@ -1,15 +1,16 @@ /* -* format-docx.ts -* -* Copyright (C) 2020-2022 Posit Software, PBC -* -*/ + * format-docx.ts + * + * Copyright (C) 2020-2022 Posit Software, PBC + */ import { kFilterParams } from "../../config/constants.ts"; -import { Format } from "../../config/types.ts"; +import { Format, FormatExtras, PandocFlags } from "../../config/types.ts"; import { mergeConfigs } from "../../core/config.ts"; import { formatResourcePath } from "../../core/resources.ts"; import { createWordprocessorFormat } from "../formats-shared.ts"; +import { unzip, zip } from "../../core/zip.ts"; +import { join, resolve } from "../../deno_ral/path.ts"; const kIconCaution = "icon-caution"; const kIconImportant = "icon-important"; @@ -17,12 +18,19 @@ const kIconNote = "icon-note"; const kIconTip = "icon-tip"; const kIconWarning = "icon-warning"; +const kLineNumbers = "line-numbers"; + export function docxFormat(): Format { return mergeConfigs( createWordprocessorFormat("MS Word", "docx"), { - formatExtras: () => { - return { + formatExtras: ( + _input: string, + _markdown: string, + _flags: PandocFlags, + format: Format, + ): FormatExtras => { + const extras: FormatExtras = { [kFilterParams]: { [kIconCaution]: iconPath("caution.png"), [kIconImportant]: iconPath("important.png"), @@ -31,6 +39,15 @@ export function docxFormat(): Format { [kIconWarning]: iconPath("warning.png"), }, }; + // `line-numbers: true` enables continuous Word line numbering. Word + // stores it in each section's , which pandoc writes from the + // reference doc after filters run, so it can only be applied by a + // post-processor on the finished file (see addDocxLineNumbers). + const lineNumbers = format.metadata[kLineNumbers]; + if (lineNumbers === true || lineNumbers === "true") { + extras.postprocessors = [addDocxLineNumbers]; + } + return extras; }, extensions: { book: { @@ -44,3 +61,35 @@ export function docxFormat(): Format { function iconPath(icon: string) { return formatResourcePath("docx", icon); } + +// Turn on continuous line numbering by adding to the section +// properties () already present in the rendered document. Because we +// edit the existing sections — which carry the header references and page size +// — the running head and paper size (Letter, A4, etc.) are preserved. +async function addDocxLineNumbers( + output: string, +): Promise<{ supporting?: string[]; resources?: string[] } | void> { + const outputPath = resolve(output); + const tempDir = await Deno.makeTempDir(); + try { + await unzip(outputPath, tempDir); + const documentXml = join(tempDir, "word", "document.xml"); + const xml = await Deno.readTextFile(documentXml); + // Add line numbering to each section that lacks it (idempotent). The regex + // assumes sections are not nested, which holds for pandoc/Quarto output. + const updated = xml.replace( + //g, + (sectPr) => + sectPr.includes("", + '', + ), + ); + await Deno.writeTextFile(documentXml, updated); + // Repackage the edited document parts back into the .docx. + await Deno.remove(outputPath); + await zip(["."], outputPath, { cwd: tempDir }); + } finally { + await Deno.remove(tempDir, { recursive: true }); + } +} diff --git a/src/resources/schema/document-options.yml b/src/resources/schema/document-options.yml index 1ec0009661a..8a308248b70 100644 --- a/src/resources/schema/document-options.yml +++ b/src/resources/schema/document-options.yml @@ -6,6 +6,14 @@ Use the specified file as a style reference in producing a docx, pptx, or odt file. +- name: line-numbers + tags: + formats: [docx] + schema: boolean + default: false + description: | + Add continuous line numbering to the margins of the document. + - name: brand schema: ref: brand-path-bool-light-dark diff --git a/tests/docs/smoke-all/2026/06/27/docx-line-numbers.qmd b/tests/docs/smoke-all/2026/06/27/docx-line-numbers.qmd new file mode 100644 index 00000000000..d9dbd92268a --- /dev/null +++ b/tests/docs/smoke-all/2026/06/27/docx-line-numbers.qmd @@ -0,0 +1,17 @@ +--- +format: + docx: + line-numbers: true +_quarto: + tests: + docx: + ensureDocxXpath: + - + - "//w:lnNumType" + - [] +--- + +# Section + +A paragraph of text so that the rendered Word document has several lines, which +`line-numbers: true` should number continuously in the margin.