documents.js 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,7 +116,7 @@ const pdfFromXlsx = xlsxToPdf(xlsxBytes); // composes xlsxToOds -> odsToPdf inte
116
116
  const xlsxBytes2 = pdfToXlsx(pdfFromXlsx); // composes pdfToOds -> odsToXlsx internally
117
117
 
118
118
  const pdfFromMarkdown = markdownToPdf(markdownBytes);
119
- const markdownBytes2 = pdfToMarkdown(pdfFromMarkdown); // the lossiest conversion in the whole package -- see Fidelity
119
+ const markdownBytes2 = pdfToMarkdown(pdfFromMarkdown); // the lossiest conversion in the whole package -- see Fidelity; it does carry page boundaries (one '<!-- page break -->' marker per page) and rank-inferred heading levels
120
120
 
121
121
  const pdfFromCsv = csvToPdf(csvBytes); // composes csvToOds -> odsToPdf internally
122
122
  const csvBytes2 = pdfToCsv(pdfFromCsv); // composes pdfToOds -> odsToCsv internally; recovers what was printed, then heuristically re-types it
@@ -127,6 +127,8 @@ const svgBytes2 = pdfToSvg(pdfFromSvg); // readPdf -> reconstructDrawing -> buil
127
127
 
128
128
  Each accepts an optional `signal` (`AbortSignal`) and either `onSubstitution` (X → PDF, called per character not representable in a standard-14 font) or `sink` (PDF → X, called per recoverable parse diagnostic). Every X → PDF conversion additionally accepts `fonts` (extra `ProvidedFont` faces) and `onFontSubstitution` (per family+weight+style that resolved to something else). Neither is needed for the common case — see [Fonts](#fonts).
129
129
 
130
+ **Cancellation granularity, for CPU-metered runtimes** (ExaDev/documents.js#585): every conversion here is synchronous end to end, and the `signal` is honoured at page boundaries — once per page in pdf-codec's `readPdf`/`writePdf` page loops and once per page in each of this package's four reconstructors (wordprocessing/presentation/drawing/spreadsheet). An abort arriving mid-conversion therefore takes effect at the next page boundary, not instantly: a single page's content-stream interpretation, pdf-codec's document-open phase, and the per-target build/encode stage after reconstruction are not interruptible, and parse cost is roughly linear in decompressed content length — budget for the worst single page, not the page count. A shared `AbortSignal` makes a deadline enforceable at that granularity on Cloudflare Workers; it cannot convert a synchronous conversion into a resumable or streaming one (an async page-at-a-time API is a deliberate non-goal of the current surface — see pdf-codec's README for the same statement from the codec side).
131
+
130
132
  ### Cross-format bridges
131
133
 
132
134
  Twenty-four bridge functions across twelve pairs bypass the PDF pivot where a direct path exists. Eight same-variant direct-copy pairs (`odtToDocx`/`docxToOdt`, `odpToPptx`/`pptxToOdp`, `odsToXlsx`/`xlsxToOds`, `csvToOds`/`odsToCsv`, `csvToXlsx`/`xlsxToCsv`, `svgToOdg`/`odgToSvg`, `markdownToDocx`/`docxToMarkdown`, `markdownToOdt`/`odtToMarkdown`) compose a direct `readXContent` → `buildYPackage` pivot copy — the csv pairs are one hop to its spreadsheet siblings, so csv never needs PDF to reach ods or xlsx, and `svgToOdg`/`odgToSvg` bridge svg to its drawing sibling odg the same way. Two cross-variant semantic-transform pairs (`docxToPptx`/`pptxToDocx`, `odtToOdp`/`odpToOdt`) go through `src/convert/variant-bridges.ts`. Two PDF-composed pairs (`xlsxToMarkdown`/`markdownToXlsx`, `csvToMarkdown`/`markdownToCsv`) route through PDF internally — the lossiest conversions in the package.
@@ -716,7 +718,7 @@ Read as **row → column**. `✓` lossless, `~` bounded, `✗` lossy, `✗✗` s
716
718
 
717
719
  **PDF → ods** recovers what was printed, not what was entered. The printed string always survives in `displayText`; re-typed `value` is explicitly probabilistic inference.
718
720
 
719
- **`markdownToPdf`/`pdfToMarkdown`** is the lossiest round trip: `markdownToPdf` is faithful, but `pdfToMarkdown` stacks reconstruction lossiness PLUS markdown's coarser vocabulary (no colour, font, size, alignment). The PDF-composed markdown bridges (`xlsxToMarkdown`/`markdownToXlsx`, `csvToMarkdown`/`markdownToCsv`) stack the same two losses in both directions — hence their `✗✗` cells.
721
+ **`markdownToPdf`/`pdfToMarkdown`** is the lossiest round trip: `markdownToPdf` is faithful, but `pdfToMarkdown` stacks reconstruction lossiness PLUS markdown's coarser vocabulary (no colour, font, size, alignment). Two structure signals do survive it (ExaDev/documents.js#584): every page boundary arrives as an exact `<!-- page break -->` marker (one per boundary — the one fact a rendered PDF states precisely), and headings are inferred from font size — each distinct size at least 2pt above the document's modal body size is a heading, ranked largest-first into `Heading1..6`, which inverts this package's own heading render sizes exactly and is a heuristic (the well-worn "largest text is the title" reading) for any other producer. Tables reach markdown as GFM pipe tables wherever the gridline-lattice gate succeeds; a table rendered without drawn gridlines (which includes every `markdownToPdf`-authored one, markdown carrying no border concept) correctly comes back as tab-separated prose rather than invented structure. The PDF-composed markdown bridges (`xlsxToMarkdown`/`markdownToXlsx`, `csvToMarkdown`/`markdownToCsv`) stack the same two losses in both directions — hence their `✗✗` cells.
720
722
 
721
723
  **The six same-variant bridge pairs** (odt⇄docx, odp⇄pptx, ods⇄xlsx, csv⇄ods, csv⇄xlsx, svg⇄odg) bypass PDF entirely — no layout engine, no reconstruction. Text, styling, tables, lists, rotated shapes survive completely. `ods⇄xlsx` has small format-boundary limits (time cells, formula dialects). Embedded formulas survive `odtToDocx` as real OOXML math. The csv pairs are bounded by what csv itself carries: toward ods/xlsx nothing the csv had is lost, while writing to csv collapses each cell to its `displayText` — formulas become their rendered values, formatting disappears, and a multi-sheet source must name the sheet it wants. The svg pair carries the six vector primitives losslessly in both directions; its one asymmetry is paint defaults — SVG's absent-fill-is-black versus a drawing frame's no-fill.
722
724
 
@@ -5,8 +5,8 @@ const require_edit_odt_content = require("../edit/odt/content.cjs");
5
5
  const require_edit_odp_content = require("../edit/odp/content.cjs");
6
6
  const require_edit_ods_content = require("../edit/ods/content.cjs");
7
7
  const require_edit_odg_content = require("../edit/odg/content.cjs");
8
- const require_markdown_read = require("../markdown/read.cjs");
9
8
  const require_markdown_write = require("../markdown/write.cjs");
9
+ const require_markdown_read = require("../markdown/read.cjs");
10
10
  const require_ooxml_docx_read = require("../ooxml/docx/read.cjs");
11
11
  const require_ooxml_pptx_read = require("../ooxml/pptx/read.cjs");
12
12
  const require_odf_formula_read = require("../odf/formula/read.cjs");
@@ -4,8 +4,8 @@ import { buildOdtPackage } from "../edit/odt/content.js";
4
4
  import { buildOdpPackage } from "../edit/odp/content.js";
5
5
  import { buildOdsPackage } from "../edit/ods/content.js";
6
6
  import { buildOdgPackage } from "../edit/odg/content.js";
7
- import { readMarkdownContent } from "../markdown/read.js";
8
7
  import { buildMarkdownText } from "../markdown/write.js";
8
+ import { readMarkdownContent } from "../markdown/read.js";
9
9
  import { readDocxContent as readDocxContent$1 } from "../ooxml/docx/read.js";
10
10
  import { readPptxContent as readPptxContent$1 } from "../ooxml/pptx/read.js";
11
11
  import { readOdfFormulaContent } from "../odf/formula/read.js";
@@ -5,8 +5,8 @@ const require_edit_odt_content = require("../edit/odt/content.cjs");
5
5
  const require_edit_odp_content = require("../edit/odp/content.cjs");
6
6
  const require_edit_ods_content = require("../edit/ods/content.cjs");
7
7
  const require_edit_odg_content = require("../edit/odg/content.cjs");
8
- const require_markdown_read = require("../markdown/read.cjs");
9
8
  const require_markdown_write = require("../markdown/write.cjs");
9
+ const require_markdown_read = require("../markdown/read.cjs");
10
10
  const require_ooxml_docx_read = require("../ooxml/docx/read.cjs");
11
11
  const require_ooxml_pptx_read = require("../ooxml/pptx/read.cjs");
12
12
  const require_odf_odt_read = require("../odf/odt/read.cjs");
@@ -4,8 +4,8 @@ import { buildOdtPackage } from "../edit/odt/content.js";
4
4
  import { buildOdpPackage } from "../edit/odp/content.js";
5
5
  import { buildOdsPackage } from "../edit/ods/content.js";
6
6
  import { buildOdgPackage } from "../edit/odg/content.js";
7
- import { readMarkdownContent as readMarkdownContent$1 } from "../markdown/read.js";
8
7
  import { buildMarkdownText } from "../markdown/write.js";
8
+ import { readMarkdownContent as readMarkdownContent$1 } from "../markdown/read.js";
9
9
  import { readDocxContent as readDocxContent$1 } from "../ooxml/docx/read.js";
10
10
  import { readPptxContent as readPptxContent$1 } from "../ooxml/pptx/read.js";
11
11
  import { readOdtContent as readOdtContent$1 } from "../odf/odt/read.js";
@@ -123,6 +123,7 @@ function populateParagraph(paragraph, block) {
123
123
  paragraph.styleId = block.styleId;
124
124
  paragraph.alignment = block.alignment;
125
125
  paragraph.list = block.list;
126
+ if (block.headingLevel !== void 0) paragraph.headingLevel = block.headingLevel;
126
127
  if (block.spacingBeforePt !== void 0) paragraph.spacingBeforePt = block.spacingBeforePt;
127
128
  if (block.spacingAfterPt !== void 0) paragraph.spacingAfterPt = block.spacingAfterPt;
128
129
  if (block.lineSpacing !== void 0) paragraph.lineSpacing = block.lineSpacing;
@@ -122,6 +122,7 @@ function populateParagraph(paragraph, block) {
122
122
  paragraph.styleId = block.styleId;
123
123
  paragraph.alignment = block.alignment;
124
124
  paragraph.list = block.list;
125
+ if (block.headingLevel !== void 0) paragraph.headingLevel = block.headingLevel;
125
126
  if (block.spacingBeforePt !== void 0) paragraph.spacingBeforePt = block.spacingBeforePt;
126
127
  if (block.spacingAfterPt !== void 0) paragraph.spacingAfterPt = block.spacingAfterPt;
127
128
  if (block.lineSpacing !== void 0) paragraph.lineSpacing = block.lineSpacing;
@@ -208,6 +208,22 @@ var DocxParagraph = class {
208
208
  const numPr = require_xml_edit.getOrCreateChildElement(pPr, "w:numPr", require_edit_docx_props.PPR_ORDER, () => require_xml_fragment.el("w:numPr"));
209
209
  numPr.children = [require_xml_fragment.el("w:ilvl", { "w:val": String(value.level) }), require_xml_fragment.el("w:numId", { "w:val": value.numId })];
210
210
  }
211
+ get headingLevel() {
212
+ const pPr = this.pPr(false);
213
+ const outlineLvl = pPr === void 0 ? void 0 : directChild(pPr, "w:outlineLvl");
214
+ const val = outlineLvl === void 0 ? void 0 : (0, ooxml_js.attr)(outlineLvl, "w:val");
215
+ return val === void 0 ? void 0 : Number(val) + 1;
216
+ }
217
+ set headingLevel(value) {
218
+ if (value === void 0) {
219
+ const pPr = this.pPr(false);
220
+ const existing = pPr === void 0 ? void 0 : directChild(pPr, "w:outlineLvl");
221
+ if (existing !== void 0 && pPr !== void 0) require_xml_edit.removeChild(pPr.children, existing);
222
+ return;
223
+ }
224
+ const outlineLvl = require_xml_edit.getOrCreateChildElement(this.pPr(true), "w:outlineLvl", require_edit_docx_props.PPR_ORDER, () => require_xml_fragment.el("w:outlineLvl"));
225
+ require_xml_edit.setAttr(outlineLvl, "w:val", String(value - 1));
226
+ }
211
227
  appendOfficeMath(mathml) {
212
228
  const node = this.live();
213
229
  const result = require_omml_write.buildOfficeMathParagraph(mathml);
@@ -256,10 +272,11 @@ var DocxParagraph = class {
256
272
  };
257
273
  function buildParagraph(init = {}) {
258
274
  const paragraph = require_xml_fragment.el("w:p");
259
- if (init.styleId !== void 0 || init.alignment !== void 0) {
275
+ if (init.styleId !== void 0 || init.alignment !== void 0 || init.headingLevel !== void 0) {
260
276
  const pPr = require_xml_fragment.el("w:pPr");
261
277
  if (init.styleId !== void 0) pPr.children.push(require_xml_fragment.el("w:pStyle", { "w:val": init.styleId }));
262
278
  if (init.alignment !== void 0) pPr.children.push(require_xml_fragment.el("w:jc", { "w:val": init.alignment === "justify" ? "both" : init.alignment }));
279
+ if (init.headingLevel !== void 0) pPr.children.push(require_xml_fragment.el("w:outlineLvl", { "w:val": String(init.headingLevel - 1) }));
263
280
  paragraph.children.push(pPr);
264
281
  }
265
282
  if (init.text !== void 0) paragraph.children.push(require_edit_docx_run.buildRun({ text: init.text }));
@@ -8,6 +8,7 @@ import { Package, XmlElement, XmlNode } from "ooxml.js";
8
8
  interface ParagraphInit {
9
9
  readonly text?: string;
10
10
  readonly styleId?: string;
11
+ readonly headingLevel?: number;
11
12
  readonly alignment?: 'left' | 'center' | 'right' | 'justify';
12
13
  }
13
14
  interface ImageMediaContext {
@@ -47,6 +48,8 @@ declare class DocxParagraph {
47
48
  set indentFirstLinePt(value: number | undefined);
48
49
  get list(): ContentListMembership | undefined;
49
50
  set list(value: ContentListMembership | undefined);
51
+ get headingLevel(): number | undefined;
52
+ set headingLevel(value: number | undefined);
50
53
  appendOfficeMath(mathml: readonly MathMlNode$1[]): OmmlWriteResult & {
51
54
  readonly written: boolean;
52
55
  };
@@ -8,6 +8,7 @@ import { ContentListMembership, ContentVector } from "document-schema.js";
8
8
  interface ParagraphInit {
9
9
  readonly text?: string;
10
10
  readonly styleId?: string;
11
+ readonly headingLevel?: number;
11
12
  readonly alignment?: 'left' | 'center' | 'right' | 'justify';
12
13
  }
13
14
  interface ImageMediaContext {
@@ -47,6 +48,8 @@ declare class DocxParagraph {
47
48
  set indentFirstLinePt(value: number | undefined);
48
49
  get list(): ContentListMembership | undefined;
49
50
  set list(value: ContentListMembership | undefined);
51
+ get headingLevel(): number | undefined;
52
+ set headingLevel(value: number | undefined);
50
53
  appendOfficeMath(mathml: readonly MathMlNode$1[]): OmmlWriteResult & {
51
54
  readonly written: boolean;
52
55
  };
@@ -207,6 +207,22 @@ var DocxParagraph = class {
207
207
  const numPr = getOrCreateChildElement(pPr, "w:numPr", PPR_ORDER, () => el("w:numPr"));
208
208
  numPr.children = [el("w:ilvl", { "w:val": String(value.level) }), el("w:numId", { "w:val": value.numId })];
209
209
  }
210
+ get headingLevel() {
211
+ const pPr = this.pPr(false);
212
+ const outlineLvl = pPr === void 0 ? void 0 : directChild(pPr, "w:outlineLvl");
213
+ const val = outlineLvl === void 0 ? void 0 : attr(outlineLvl, "w:val");
214
+ return val === void 0 ? void 0 : Number(val) + 1;
215
+ }
216
+ set headingLevel(value) {
217
+ if (value === void 0) {
218
+ const pPr = this.pPr(false);
219
+ const existing = pPr === void 0 ? void 0 : directChild(pPr, "w:outlineLvl");
220
+ if (existing !== void 0 && pPr !== void 0) removeChild(pPr.children, existing);
221
+ return;
222
+ }
223
+ const outlineLvl = getOrCreateChildElement(this.pPr(true), "w:outlineLvl", PPR_ORDER, () => el("w:outlineLvl"));
224
+ setAttr(outlineLvl, "w:val", String(value - 1));
225
+ }
210
226
  appendOfficeMath(mathml) {
211
227
  const node = this.live();
212
228
  const result = buildOfficeMathParagraph(mathml);
@@ -255,10 +271,11 @@ var DocxParagraph = class {
255
271
  };
256
272
  function buildParagraph(init = {}) {
257
273
  const paragraph = el("w:p");
258
- if (init.styleId !== void 0 || init.alignment !== void 0) {
274
+ if (init.styleId !== void 0 || init.alignment !== void 0 || init.headingLevel !== void 0) {
259
275
  const pPr = el("w:pPr");
260
276
  if (init.styleId !== void 0) pPr.children.push(el("w:pStyle", { "w:val": init.styleId }));
261
277
  if (init.alignment !== void 0) pPr.children.push(el("w:jc", { "w:val": init.alignment === "justify" ? "both" : init.alignment }));
278
+ if (init.headingLevel !== void 0) pPr.children.push(el("w:outlineLvl", { "w:val": String(init.headingLevel - 1) }));
262
279
  paragraph.children.push(pPr);
263
280
  }
264
281
  if (init.text !== void 0) paragraph.children.push(buildRun({ text: init.text }));
@@ -24,6 +24,7 @@ const PPR_ORDER = [
24
24
  "w:spacing",
25
25
  "w:ind",
26
26
  "w:jc",
27
+ "w:outlineLvl",
27
28
  "w:rPr"
28
29
  ];
29
30
  function ensureFirstChild(parent, tag) {
@@ -23,6 +23,7 @@ const PPR_ORDER = [
23
23
  "w:spacing",
24
24
  "w:ind",
25
25
  "w:jc",
26
+ "w:outlineLvl",
26
27
  "w:rPr"
27
28
  ];
28
29
  function ensureFirstChild(parent, tag) {
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_model_metadata = require("../../model/metadata.cjs");
3
3
  const require_ports_clock = require("../../ports/clock.cjs");
4
- const require_markdown_read = require("../../markdown/read.cjs");
5
4
  const require_markdown_write = require("../../markdown/write.cjs");
5
+ const require_markdown_read = require("../../markdown/read.cjs");
6
6
  const require_edit_markdown_paragraph = require("./paragraph.cjs");
7
7
  const require_edit_markdown_list = require("./list.cjs");
8
8
  const require_edit_markdown_table = require("./table.cjs");
@@ -1,7 +1,7 @@
1
1
  import { resolveMetadataTimestamps } from "../../model/metadata.js";
2
2
  import { systemClock } from "../../ports/clock.js";
3
- import { readMarkdownContent as readMarkdownContent$1 } from "../../markdown/read.js";
4
3
  import { buildMarkdownText } from "../../markdown/write.js";
4
+ import { readMarkdownContent as readMarkdownContent$1 } from "../../markdown/read.js";
5
5
  import { MarkdownParagraph, buildParagraph } from "./paragraph.js";
6
6
  import { MarkdownList } from "./list.js";
7
7
  import { MarkdownTable, buildTable } from "./table.js";
package/dist/index.cjs CHANGED
@@ -39,8 +39,8 @@ const require_edit_odg_editor = require("./edit/odg/editor.cjs");
39
39
  const require_edit_odg_content = require("./edit/odg/content.cjs");
40
40
  const require_latex_lower = require("./latex/lower.cjs");
41
41
  const require_markdown_math = require("./markdown/math.cjs");
42
- const require_markdown_read = require("./markdown/read.cjs");
43
42
  const require_markdown_write = require("./markdown/write.cjs");
43
+ const require_markdown_read = require("./markdown/read.cjs");
44
44
  const require_edit_markdown_run = require("./edit/markdown/run.cjs");
45
45
  const require_edit_markdown_paragraph = require("./edit/markdown/paragraph.cjs");
46
46
  const require_edit_markdown_list = require("./edit/markdown/list.cjs");
package/dist/index.js CHANGED
@@ -38,8 +38,8 @@ import { OdgEditor, createOdg, openOdg } from "./edit/odg/editor.js";
38
38
  import { buildOdgPackage } from "./edit/odg/content.js";
39
39
  import { latexToFormula, lowerLatex } from "./latex/lower.js";
40
40
  import { lowerMarkdownMath } from "./markdown/math.js";
41
- import { readMarkdownContent } from "./markdown/read.js";
42
41
  import { MarkdownConstructUnsupportedError, buildMarkdownText } from "./markdown/write.js";
42
+ import { readMarkdownContent } from "./markdown/read.js";
43
43
  import { MarkdownRun } from "./edit/markdown/run.js";
44
44
  import { MarkdownParagraph } from "./edit/markdown/paragraph.js";
45
45
  import { MarkdownList } from "./edit/markdown/list.js";
@@ -42,8 +42,8 @@ function textItemVerticalExtent(item) {
42
42
  function textItemToContentRun(item) {
43
43
  return {
44
44
  text: item.text,
45
- bold: item.font.weight === "bold" ? true : void 0,
46
- italic: item.font.style === "italic" ? true : void 0,
45
+ ...item.font.weight === "bold" ? { bold: true } : {},
46
+ ...item.font.style === "italic" ? { italic: true } : {},
47
47
  fontFamily: item.font.family,
48
48
  sizePt: item.sizePt,
49
49
  color: item.color
@@ -134,33 +134,55 @@ function estimateModalLineSpacing(lines) {
134
134
  }
135
135
  function reconstructWordprocessing(doc, options) {
136
136
  const signal = options?.signal;
137
+ const headingLevels = headingSizeLevels(doc);
137
138
  const sections = [];
138
139
  let currentGroup = [];
139
140
  let groupStartPageIndex = 0;
140
141
  for (const page of doc.pages) {
141
142
  require_ports_abort.throwIfAborted(signal);
142
143
  if (currentGroup.length > 0 && !samePageSize(currentGroup[0], page)) {
143
- sections.push(buildSection(currentGroup, groupStartPageIndex, doc.images));
144
+ sections.push(buildSection(currentGroup, groupStartPageIndex, doc.images, headingLevels));
144
145
  groupStartPageIndex += currentGroup.length;
145
146
  currentGroup = [];
146
147
  }
147
148
  currentGroup.push(page);
148
149
  }
149
- if (currentGroup.length > 0) sections.push(buildSection(currentGroup, groupStartPageIndex, doc.images));
150
+ if (currentGroup.length > 0) sections.push(buildSection(currentGroup, groupStartPageIndex, doc.images, headingLevels));
150
151
  return {
151
152
  kind: "wordprocessing",
152
153
  metadata: doc.metadata,
153
154
  sections
154
155
  };
155
156
  }
157
+ const HEADING_MIN_SIZE_DELTA_PT = 2;
158
+ const MAX_HEADING_LEVEL = 6;
159
+ function headingSizeLevels(doc) {
160
+ const sizes = [];
161
+ for (const page of doc.pages) for (const item of page.items) if (item.kind === "text") sizes.push(item.sizePt);
162
+ if (sizes.length === 0) return /* @__PURE__ */ new Map();
163
+ const bodySizePt = modeOf(sizes, .5);
164
+ const headingBuckets = /* @__PURE__ */ new Set();
165
+ for (const size of sizes) {
166
+ const bucket = Math.round(size / .5) * .5;
167
+ if (bucket - bodySizePt >= HEADING_MIN_SIZE_DELTA_PT) headingBuckets.add(bucket);
168
+ }
169
+ const levels = /* @__PURE__ */ new Map();
170
+ [...headingBuckets].sort((a, b) => b - a).forEach((bucket, index) => {
171
+ levels.set(bucket, Math.min(index + 1, MAX_HEADING_LEVEL));
172
+ });
173
+ return levels;
174
+ }
175
+ function headingLevelOf(paragraph, levels) {
176
+ return levels.get(modeOf(paragraph.lines.flatMap((line) => line.items.map((item) => item.sizePt)), .5));
177
+ }
156
178
  function samePageSize(a, b) {
157
179
  return a.widthPt === b.widthPt && a.heightPt === b.heightPt;
158
180
  }
159
- function buildSection(pages, startPageIndex, images) {
181
+ function buildSection(pages, startPageIndex, images, headingLevels) {
160
182
  const blocks = [];
161
183
  pages.forEach((page, i) => {
162
184
  if (i > 0) blocks.push({ kind: "pageBreak" });
163
- blocks.push(...reconstructPageBlocks(page, startPageIndex + i, images));
185
+ blocks.push(...reconstructPageBlocks(page, startPageIndex + i, images, headingLevels));
164
186
  });
165
187
  return {
166
188
  pageSize: {
@@ -171,7 +193,7 @@ function buildSection(pages, startPageIndex, images) {
171
193
  blocks
172
194
  };
173
195
  }
174
- function reconstructPageBlocks(page, pageIndex, images) {
196
+ function reconstructPageBlocks(page, pageIndex, images, headingLevels) {
175
197
  const recoveredTable = recoverTable(page, pageIndex);
176
198
  const consumedText = recoveredTable?.consumedText;
177
199
  const textItems = page.items.filter((i) => i.kind === "text" && consumedText?.has(i) !== true);
@@ -180,7 +202,7 @@ function reconstructPageBlocks(page, pageIndex, images) {
180
202
  const positioned = [];
181
203
  for (const paragraph of paragraphs) positioned.push({
182
204
  yPt: paragraph.lines[0].baselineY,
183
- block: paragraphToContentParagraph(paragraph, pageIndex)
205
+ block: paragraphToContentParagraph(paragraph, pageIndex, headingLevelOf(paragraph, headingLevels))
184
206
  });
185
207
  for (const img of imageItems) {
186
208
  const asset = images[img.imageId];
@@ -231,6 +253,7 @@ function clusterIntoParagraphs(lines) {
231
253
  }
232
254
  function startsNewParagraph(prev, next, modalSpacing, dominantLeftX) {
233
255
  if (prev.baselineY - next.baselineY > PARAGRAPH_GAP_MULTIPLIER * modalSpacing) return true;
256
+ if (!fontSizesClose(prev.items[0].sizePt, next.items[0].sizePt)) return true;
234
257
  const nextLeft = next.items[0].xPt;
235
258
  const prevLeft = prev.items[0].xPt;
236
259
  const emPt = next.items[0].sizePt;
@@ -239,12 +262,16 @@ function startsNewParagraph(prev, next, modalSpacing, dominantLeftX) {
239
262
  return nextIndented && prevAtMargin;
240
263
  }
241
264
  const LEFT_ALIGN_TOLERANCE_PT = 2;
242
- function paragraphToContentParagraph(paragraph, pageIndex) {
265
+ function paragraphToContentParagraph(paragraph, pageIndex, headingLevel) {
243
266
  const dominantLeftX = modeOf(paragraph.lines.map((l) => l.items[0].xPt), 1);
244
267
  const result = {
245
268
  kind: "paragraph",
246
269
  runs: [],
247
- alignment: paragraph.lines.every((l) => Math.abs(l.items[0].xPt - dominantLeftX) <= LEFT_ALIGN_TOLERANCE_PT) ? "left" : void 0
270
+ alignment: paragraph.lines.every((l) => Math.abs(l.items[0].xPt - dominantLeftX) <= LEFT_ALIGN_TOLERANCE_PT) ? "left" : void 0,
271
+ ...headingLevel !== void 0 ? {
272
+ styleId: `Heading${String(headingLevel)}`,
273
+ headingLevel
274
+ } : {}
248
275
  };
249
276
  paragraph.lines.forEach((line, lineIndex) => {
250
277
  require_layout_shared.stampFrame(result, pageIndex, lineBox(line, pageIndex));
@@ -254,6 +281,12 @@ function paragraphToContentParagraph(paragraph, pageIndex) {
254
281
  }
255
282
  pushRunsForLine(result.runs, line, pageIndex);
256
283
  });
284
+ if (headingLevel !== void 0) result.runs = result.runs.map((run) => {
285
+ if (run.bold !== true) return run;
286
+ const withoutBold = { ...run };
287
+ delete withoutBold.bold;
288
+ return withoutBold;
289
+ });
257
290
  return result;
258
291
  }
259
292
  function reconstructPresentation(doc, options) {
@@ -41,8 +41,8 @@ function textItemVerticalExtent(item) {
41
41
  function textItemToContentRun(item) {
42
42
  return {
43
43
  text: item.text,
44
- bold: item.font.weight === "bold" ? true : void 0,
45
- italic: item.font.style === "italic" ? true : void 0,
44
+ ...item.font.weight === "bold" ? { bold: true } : {},
45
+ ...item.font.style === "italic" ? { italic: true } : {},
46
46
  fontFamily: item.font.family,
47
47
  sizePt: item.sizePt,
48
48
  color: item.color
@@ -133,33 +133,55 @@ function estimateModalLineSpacing(lines) {
133
133
  }
134
134
  function reconstructWordprocessing(doc, options) {
135
135
  const signal = options?.signal;
136
+ const headingLevels = headingSizeLevels(doc);
136
137
  const sections = [];
137
138
  let currentGroup = [];
138
139
  let groupStartPageIndex = 0;
139
140
  for (const page of doc.pages) {
140
141
  throwIfAborted(signal);
141
142
  if (currentGroup.length > 0 && !samePageSize(currentGroup[0], page)) {
142
- sections.push(buildSection(currentGroup, groupStartPageIndex, doc.images));
143
+ sections.push(buildSection(currentGroup, groupStartPageIndex, doc.images, headingLevels));
143
144
  groupStartPageIndex += currentGroup.length;
144
145
  currentGroup = [];
145
146
  }
146
147
  currentGroup.push(page);
147
148
  }
148
- if (currentGroup.length > 0) sections.push(buildSection(currentGroup, groupStartPageIndex, doc.images));
149
+ if (currentGroup.length > 0) sections.push(buildSection(currentGroup, groupStartPageIndex, doc.images, headingLevels));
149
150
  return {
150
151
  kind: "wordprocessing",
151
152
  metadata: doc.metadata,
152
153
  sections
153
154
  };
154
155
  }
156
+ const HEADING_MIN_SIZE_DELTA_PT = 2;
157
+ const MAX_HEADING_LEVEL = 6;
158
+ function headingSizeLevels(doc) {
159
+ const sizes = [];
160
+ for (const page of doc.pages) for (const item of page.items) if (item.kind === "text") sizes.push(item.sizePt);
161
+ if (sizes.length === 0) return /* @__PURE__ */ new Map();
162
+ const bodySizePt = modeOf(sizes, .5);
163
+ const headingBuckets = /* @__PURE__ */ new Set();
164
+ for (const size of sizes) {
165
+ const bucket = Math.round(size / .5) * .5;
166
+ if (bucket - bodySizePt >= HEADING_MIN_SIZE_DELTA_PT) headingBuckets.add(bucket);
167
+ }
168
+ const levels = /* @__PURE__ */ new Map();
169
+ [...headingBuckets].sort((a, b) => b - a).forEach((bucket, index) => {
170
+ levels.set(bucket, Math.min(index + 1, MAX_HEADING_LEVEL));
171
+ });
172
+ return levels;
173
+ }
174
+ function headingLevelOf(paragraph, levels) {
175
+ return levels.get(modeOf(paragraph.lines.flatMap((line) => line.items.map((item) => item.sizePt)), .5));
176
+ }
155
177
  function samePageSize(a, b) {
156
178
  return a.widthPt === b.widthPt && a.heightPt === b.heightPt;
157
179
  }
158
- function buildSection(pages, startPageIndex, images) {
180
+ function buildSection(pages, startPageIndex, images, headingLevels) {
159
181
  const blocks = [];
160
182
  pages.forEach((page, i) => {
161
183
  if (i > 0) blocks.push({ kind: "pageBreak" });
162
- blocks.push(...reconstructPageBlocks(page, startPageIndex + i, images));
184
+ blocks.push(...reconstructPageBlocks(page, startPageIndex + i, images, headingLevels));
163
185
  });
164
186
  return {
165
187
  pageSize: {
@@ -170,7 +192,7 @@ function buildSection(pages, startPageIndex, images) {
170
192
  blocks
171
193
  };
172
194
  }
173
- function reconstructPageBlocks(page, pageIndex, images) {
195
+ function reconstructPageBlocks(page, pageIndex, images, headingLevels) {
174
196
  const recoveredTable = recoverTable(page, pageIndex);
175
197
  const consumedText = recoveredTable?.consumedText;
176
198
  const textItems = page.items.filter((i) => i.kind === "text" && consumedText?.has(i) !== true);
@@ -179,7 +201,7 @@ function reconstructPageBlocks(page, pageIndex, images) {
179
201
  const positioned = [];
180
202
  for (const paragraph of paragraphs) positioned.push({
181
203
  yPt: paragraph.lines[0].baselineY,
182
- block: paragraphToContentParagraph(paragraph, pageIndex)
204
+ block: paragraphToContentParagraph(paragraph, pageIndex, headingLevelOf(paragraph, headingLevels))
183
205
  });
184
206
  for (const img of imageItems) {
185
207
  const asset = images[img.imageId];
@@ -230,6 +252,7 @@ function clusterIntoParagraphs(lines) {
230
252
  }
231
253
  function startsNewParagraph(prev, next, modalSpacing, dominantLeftX) {
232
254
  if (prev.baselineY - next.baselineY > PARAGRAPH_GAP_MULTIPLIER * modalSpacing) return true;
255
+ if (!fontSizesClose(prev.items[0].sizePt, next.items[0].sizePt)) return true;
233
256
  const nextLeft = next.items[0].xPt;
234
257
  const prevLeft = prev.items[0].xPt;
235
258
  const emPt = next.items[0].sizePt;
@@ -238,12 +261,16 @@ function startsNewParagraph(prev, next, modalSpacing, dominantLeftX) {
238
261
  return nextIndented && prevAtMargin;
239
262
  }
240
263
  const LEFT_ALIGN_TOLERANCE_PT = 2;
241
- function paragraphToContentParagraph(paragraph, pageIndex) {
264
+ function paragraphToContentParagraph(paragraph, pageIndex, headingLevel) {
242
265
  const dominantLeftX = modeOf(paragraph.lines.map((l) => l.items[0].xPt), 1);
243
266
  const result = {
244
267
  kind: "paragraph",
245
268
  runs: [],
246
- alignment: paragraph.lines.every((l) => Math.abs(l.items[0].xPt - dominantLeftX) <= LEFT_ALIGN_TOLERANCE_PT) ? "left" : void 0
269
+ alignment: paragraph.lines.every((l) => Math.abs(l.items[0].xPt - dominantLeftX) <= LEFT_ALIGN_TOLERANCE_PT) ? "left" : void 0,
270
+ ...headingLevel !== void 0 ? {
271
+ styleId: `Heading${String(headingLevel)}`,
272
+ headingLevel
273
+ } : {}
247
274
  };
248
275
  paragraph.lines.forEach((line, lineIndex) => {
249
276
  stampFrame(result, pageIndex, lineBox(line, pageIndex));
@@ -253,6 +280,12 @@ function paragraphToContentParagraph(paragraph, pageIndex) {
253
280
  }
254
281
  pushRunsForLine(result.runs, line, pageIndex);
255
282
  });
283
+ if (headingLevel !== void 0) result.runs = result.runs.map((run) => {
284
+ if (run.bold !== true) return run;
285
+ const withoutBold = { ...run };
286
+ delete withoutBold.bold;
287
+ return withoutBold;
288
+ });
256
289
  return result;
257
290
  }
258
291
  function reconstructPresentation(doc, options) {
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_markdown_math = require("./math.cjs");
3
+ require("./write.cjs");
3
4
  let markdown_codec = require("markdown-codec");
4
5
  //#region src/markdown/read.ts
5
6
  function readMarkdownContent(text, options, math) {
@@ -8,7 +9,20 @@ function readMarkdownContent(text, options, math) {
8
9
  ...options
9
10
  });
10
11
  if (document.kind !== "wordprocessing") throw new Error("readMarkdownContent returned a non-wordprocessing ContentDocument");
11
- return require_markdown_math.lowerMarkdownMath(document, math);
12
+ return require_markdown_math.lowerMarkdownMath(promotePageBreakMarkers(document), math);
13
+ }
14
+ function promotePageBreakMarkers(document) {
15
+ return {
16
+ ...document,
17
+ sections: document.sections.map((section) => ({
18
+ ...section,
19
+ blocks: section.blocks.map(promoteBlock)
20
+ }))
21
+ };
22
+ }
23
+ function promoteBlock(block) {
24
+ if (block.kind !== "paragraph" || block.styleId !== markdown_codec.HTML_PREFORMATTED_STYLE_ID) return block;
25
+ return block.runs.map((run) => run.text).join("") === "<!-- page break -->" ? { kind: "pageBreak" } : block;
12
26
  }
13
27
  //#endregion
14
28
  exports.readMarkdownContent = readMarkdownContent;
@@ -1,5 +1,6 @@
1
1
  import { lowerMarkdownMath } from "./math.js";
2
- import { readMarkdownContent as readMarkdownContent$1 } from "markdown-codec";
2
+ import "./write.js";
3
+ import { HTML_PREFORMATTED_STYLE_ID, readMarkdownContent as readMarkdownContent$1 } from "markdown-codec";
3
4
  //#region src/markdown/read.ts
4
5
  function readMarkdownContent(text, options, math) {
5
6
  const { document } = readMarkdownContent$1(text, {
@@ -7,7 +8,20 @@ function readMarkdownContent(text, options, math) {
7
8
  ...options
8
9
  });
9
10
  if (document.kind !== "wordprocessing") throw new Error("readMarkdownContent returned a non-wordprocessing ContentDocument");
10
- return lowerMarkdownMath(document, math);
11
+ return lowerMarkdownMath(promotePageBreakMarkers(document), math);
12
+ }
13
+ function promotePageBreakMarkers(document) {
14
+ return {
15
+ ...document,
16
+ sections: document.sections.map((section) => ({
17
+ ...section,
18
+ blocks: section.blocks.map(promoteBlock)
19
+ }))
20
+ };
21
+ }
22
+ function promoteBlock(block) {
23
+ if (block.kind !== "paragraph" || block.styleId !== HTML_PREFORMATTED_STYLE_ID) return block;
24
+ return block.runs.map((run) => run.text).join("") === "<!-- page break -->" ? { kind: "pageBreak" } : block;
11
25
  }
12
26
  //#endregion
13
27
  export { readMarkdownContent };
@@ -13,6 +13,7 @@ var MarkdownConstructUnsupportedError = class extends Error {
13
13
  const MATH_BLOCK_STYLE_ID = "MathBlock";
14
14
  const MATH_INLINE_FONT_MARKER = "Cambria Math";
15
15
  const MATH_INLINE_SOURCE = "markdown:math-inline";
16
+ const PAGE_BREAK_MARKER = "<!-- page break -->";
16
17
  function formulaParagraph(formula) {
17
18
  const latex = formula.presentation?.latex;
18
19
  if (latex === void 0) return {
@@ -34,6 +35,11 @@ function formulaParagraph(formula) {
34
35
  }
35
36
  function markdownBlock(block) {
36
37
  if (block.kind === "constructStart" || block.kind === "constructEnd") throw new MarkdownConstructUnsupportedError(block);
38
+ if (block.kind === "pageBreak") return {
39
+ kind: "paragraph",
40
+ runs: [{ text: PAGE_BREAK_MARKER }],
41
+ styleId: markdown_codec.HTML_PREFORMATTED_STYLE_ID
42
+ };
37
43
  if (block.kind === "table") return {
38
44
  ...block,
39
45
  rows: block.rows.map((row) => ({
@@ -67,4 +73,5 @@ function buildMarkdownText(document, options) {
67
73
  }
68
74
  //#endregion
69
75
  exports.MarkdownConstructUnsupportedError = MarkdownConstructUnsupportedError;
76
+ exports.PAGE_BREAK_MARKER = PAGE_BREAK_MARKER;
70
77
  exports.buildMarkdownText = buildMarkdownText;
@@ -5,6 +5,7 @@ declare class MarkdownConstructUnsupportedError extends Error {
5
5
  readonly descriptorKind: ContentConstructStart['descriptor']['kind'] | undefined;
6
6
  constructor(block: ContentConstructStart | ContentConstructEnd);
7
7
  }
8
+ declare const PAGE_BREAK_MARKER = "<!-- page break -->";
8
9
  declare function buildMarkdownText(document: ContentDocument, options?: WriteMarkdownOptions): string;
9
10
  //#endregion
10
- export { MarkdownConstructUnsupportedError, buildMarkdownText };
11
+ export { MarkdownConstructUnsupportedError, PAGE_BREAK_MARKER, buildMarkdownText };
@@ -5,6 +5,7 @@ declare class MarkdownConstructUnsupportedError extends Error {
5
5
  readonly descriptorKind: ContentConstructStart['descriptor']['kind'] | undefined;
6
6
  constructor(block: ContentConstructStart | ContentConstructEnd);
7
7
  }
8
+ declare const PAGE_BREAK_MARKER = "<!-- page break -->";
8
9
  declare function buildMarkdownText(document: ContentDocument, options?: WriteMarkdownOptions): string;
9
10
  //#endregion
10
- export { MarkdownConstructUnsupportedError, buildMarkdownText };
11
+ export { MarkdownConstructUnsupportedError, PAGE_BREAK_MARKER, buildMarkdownText };
@@ -1,5 +1,5 @@
1
1
  import { formulaOfBlock, formulaPlaceholderText } from "../model/formula.js";
2
- import { MarkdownUnsupportedDocumentKindError, writeMarkdownContent } from "markdown-codec";
2
+ import { HTML_PREFORMATTED_STYLE_ID, MarkdownUnsupportedDocumentKindError, writeMarkdownContent } from "markdown-codec";
3
3
  //#region src/markdown/write.ts
4
4
  var MarkdownConstructUnsupportedError = class extends Error {
5
5
  descriptorKind;
@@ -12,6 +12,7 @@ var MarkdownConstructUnsupportedError = class extends Error {
12
12
  const MATH_BLOCK_STYLE_ID = "MathBlock";
13
13
  const MATH_INLINE_FONT_MARKER = "Cambria Math";
14
14
  const MATH_INLINE_SOURCE = "markdown:math-inline";
15
+ const PAGE_BREAK_MARKER = "<!-- page break -->";
15
16
  function formulaParagraph(formula) {
16
17
  const latex = formula.presentation?.latex;
17
18
  if (latex === void 0) return {
@@ -33,6 +34,11 @@ function formulaParagraph(formula) {
33
34
  }
34
35
  function markdownBlock(block) {
35
36
  if (block.kind === "constructStart" || block.kind === "constructEnd") throw new MarkdownConstructUnsupportedError(block);
37
+ if (block.kind === "pageBreak") return {
38
+ kind: "paragraph",
39
+ runs: [{ text: PAGE_BREAK_MARKER }],
40
+ styleId: HTML_PREFORMATTED_STYLE_ID
41
+ };
36
42
  if (block.kind === "table") return {
37
43
  ...block,
38
44
  rows: block.rows.map((row) => ({
@@ -65,4 +71,4 @@ function buildMarkdownText(document, options) {
65
71
  }, options);
66
72
  }
67
73
  //#endregion
68
- export { MarkdownConstructUnsupportedError, buildMarkdownText };
74
+ export { MarkdownConstructUnsupportedError, PAGE_BREAK_MARKER, buildMarkdownText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "documents.js",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Bidirectional docx/pptx <-> PDF conversion and a read+write editable OOXML document model, built on ooxml.js and Zod 4 codecs.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -101,12 +101,12 @@
101
101
  "packageManager": "pnpm@11.6.0",
102
102
  "dependencies": {
103
103
  "byte-codec": "^1.1.13",
104
- "document-schema.js": "^4.3.7",
104
+ "document-schema.js": "^4.5.0",
105
105
  "fflate": "^0.8.3",
106
- "markdown-codec": "^4.0.12",
107
- "odf.js": "^5.0.7",
108
- "ooxml.js": "^4.1.1",
109
- "pdf-codec": "^3.1.0",
106
+ "markdown-codec": "^4.0.13",
107
+ "odf.js": "^5.0.8",
108
+ "ooxml.js": "^4.3.0",
109
+ "pdf-codec": "^3.1.2",
110
110
  "temml": "0.13.4",
111
111
  "zod": "^4.4.3"
112
112
  },