js.documents 7.15.2 → 7.16.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
@@ -301,7 +301,7 @@ const { kind, value } = documentFromJson(
301
301
  // kind: 'DocumentTree' (here) | 'ContentDocument'
302
302
  ```
303
303
 
304
- `buildDocumentBytes` rebuilds any `DocumentFormat`'s bytes from a tree-form `DocumentTree` — it flattens once at the boundary and hands the flat form to the builders, whose signatures never changed. `'pdf'` rebuilds the pdf-codec view from the package's own frames+pages (`layoutDocumentFromPackage`, a mechanical inverse walking the flattened content and emitting `LayoutItem`s from each node's recorded placements; throwing if the package carries no `pages`), `'odf'` has no builder and throws, everything else rebuilds from the flattened `ContentDocument`. `layoutDocumentFromPackage` is exported too, for a caller wanting the rebuilt `LayoutDocument` without writing bytes. Two honest limits on the pdf rebuild, both structural properties of what a package records: a run's frames carry positions, not the wrap decisions that distributed its text across them, so a wrapped run re-renders once, whole, at its first recorded placement; and no font registry or positioned formula survives a bare package (a formula block's frame records where it sat while its glyphs render as nothing):
304
+ `buildDocumentBytes` rebuilds any `DocumentFormat`'s bytes from a tree-form `DocumentTree` — it flattens once at the boundary and hands the flat form to the builders, whose signatures never changed. `'pdf'` rebuilds the pdf-codec view from the package's own frames+pages (`layoutDocumentFromPackage`, a mechanical inverse walking the flattened content and emitting `LayoutItem`s from each node's recorded placements; throwing if the package carries no `pages`), `'odf'` has no builder and throws, everything else rebuilds from the flattened `ContentDocument`. `layoutDocumentFromPackage` is exported too, for a caller wanting the rebuilt `LayoutDocument` without writing bytes. The pdf rebuild re-derives what the recorded data genuinely determines rather than approximating: a wrapped run's frames each carry the tight measured width of the fragment the original wrap placed there, so the rebuild re-wraps the run's text against each frame's own width through the same line-breaker and standard-14 metrics its re-render draws with, reproducing the original split wherever the original also drew through those metrics; and an embedded formula is re-typeset from its own recorded MathML at its recorded frame through the identical layoutFormula pipeline the original pass ran. One honest limit remains, a structural property of what a package records: no font registry survives a bare package, so text re-renders through the standard 14 (or the caller's write-option faces) rather than the source document's own embedded faces, and a formula whose source carried no MathML at all has nothing to re-typeset:
305
305
 
306
306
  ```ts
307
307
  import { buildDocumentBytes, docxToPdf } from "documents.js";
@@ -1,6 +1,9 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_model_geometry = require("../model/geometry.cjs");
3
3
  const require_model_bytes = require("../model/bytes.cjs");
4
+ const require_model_formula = require("../model/formula.cjs");
5
+ const require_mathml_layout = require("../mathml/layout.cjs");
6
+ const require_layout_text_layout = require("../layout/text-layout.cjs");
4
7
  const require_layout_shared = require("../layout/shared.cjs");
5
8
  require("../layout/sheets.cjs");
6
9
  const require_layout_drawing = require("../layout/drawing.cjs");
@@ -12,7 +15,11 @@ let pdf_codec = require("pdf-codec");
12
15
  function buildDocumentBytes(pkg, target) {
13
16
  if (target === "pdf") {
14
17
  if (pkg.pages === void 0) throw new Error("this DocumentTree has no pages -- only a package dumped from a <format>-to-pdf or pdf-to-<format> conversion carries them; a bridge conversion's own dump (e.g. odt-to-docx) never does, so 'pdf' is not a reachable target from it");
15
- return (0, pdf_codec.writePdf)(layoutDocumentFromPackage(pkg));
18
+ const { document: layout, formulas, fonts } = packageToLayout(pkg);
19
+ return (0, pdf_codec.writePdf)(layout, formulas.length > 0 ? {
20
+ formulas,
21
+ fonts
22
+ } : { fonts });
16
23
  }
17
24
  if (require_convert_capability.READ_ONLY_FORMATS.has(target)) throw new Error(`'${target}' is a read-only format: it cannot be built from a DocumentTree, because there is no ContentDocument-to-${target} builder`);
18
25
  const content = require_codecs_registry.DOCUMENT_FORMAT_CODECS[target].content;
@@ -23,35 +30,62 @@ function pageOfFrame(state, frame) {
23
30
  return state.pages[frame.pageIndex];
24
31
  }
25
32
  function emitRun(state, run) {
26
- const frame = run.frames?.[0];
27
- if (frame === void 0) return;
28
- const page = pageOfFrame(state, frame);
29
- if (page === void 0) return;
33
+ const frames = run.frames ?? [];
30
34
  const font = require_layout_shared.runFont(run);
31
35
  const sizePt = run.sizePt ?? 18;
32
- const textItem = {
33
- kind: "text",
34
- text: run.text,
35
- xPt: frame.xPt,
36
- yPt: frame.yPt,
37
- font,
38
- sizePt,
39
- color: run.color ?? document_schema_js.COLOR_BLACK,
40
- underline: run.underline
41
- };
42
- page.items.push(textItem);
43
- if (run.hyperlink !== void 0) {
44
- const link = {
36
+ const color = run.color ?? document_schema_js.COLOR_BLACK;
37
+ const placements = frames.filter((frame) => pageOfFrame(state, frame) !== void 0);
38
+ if (placements.length === 0) return;
39
+ const fragments = placements.length === 1 ? [run.text] : rederiveWrapFragments(run.text, font, sizePt, placements, state.measurer);
40
+ for (const [index, frame] of placements.entries()) {
41
+ const page = pageOfFrame(state, frame);
42
+ if (page === void 0) continue;
43
+ const text = index === placements.length - 1 ? fragments.slice(index).join(" ") : fragments[index];
44
+ if (text !== void 0 && text !== "") page.items.push({
45
+ kind: "text",
46
+ text,
47
+ xPt: frame.xPt,
48
+ yPt: frame.yPt,
49
+ font,
50
+ sizePt,
51
+ color,
52
+ underline: run.underline
53
+ });
54
+ if (run.hyperlink !== void 0) page.items.push({
45
55
  kind: "link",
46
56
  uri: run.hyperlink,
47
57
  xPt: frame.xPt,
48
58
  yPt: frame.yPt,
49
59
  widthPt: frame.widthPt,
50
60
  heightPt: frame.heightPt
51
- };
52
- page.items.push(link);
61
+ });
53
62
  }
54
63
  }
64
+ function rederiveWrapFragments(text, font, sizePt, frames, measurer) {
65
+ let atoms = require_layout_text_layout.atomizeForWrap([{
66
+ text,
67
+ font,
68
+ sizePt,
69
+ color: document_schema_js.COLOR_BLACK
70
+ }], measurer);
71
+ let remaining = text;
72
+ const fragments = [];
73
+ for (const frame of frames) {
74
+ if (remaining === "" || atoms.length === 0) break;
75
+ const { line, rest } = require_layout_text_layout.firstWrappedLineOf(atoms, measurer, frame.widthPt);
76
+ const consumed = line.fragments.map((f) => f.text).join("");
77
+ if (!remaining.startsWith(consumed)) return [text];
78
+ remaining = remaining.slice(consumed.length).trimStart();
79
+ fragments.push(consumed.trimEnd());
80
+ atoms = trimLeadingGlueAtoms(rest);
81
+ }
82
+ return fragments;
83
+ }
84
+ function trimLeadingGlueAtoms(atoms) {
85
+ let start = 0;
86
+ while (start < atoms.length && atoms[start]?.kind === "glue") start++;
87
+ return atoms.slice(start);
88
+ }
55
89
  function emitParagraph(state, paragraph) {
56
90
  for (const run of paragraph.runs) emitRun(state, run);
57
91
  }
@@ -166,16 +200,44 @@ function emitBlocks(state, blocks) {
166
200
  for (const block of blocks) if (block.kind === "paragraph") emitParagraph(state, block);
167
201
  else if (block.kind === "image") emitImageBlock(state, block, block.frames);
168
202
  else if (block.kind === "table") emitTable(state, block);
203
+ else if (block.kind === "embeddedObject") emitEmbeddedObjectBlock(state, block);
204
+ }
205
+ function emitEmbeddedObjectBlock(state, block) {
206
+ const formula = require_model_formula.formulaOfBlock(block);
207
+ if (formula === void 0 || formula.mathml.length === 0) return;
208
+ const { metricsAt } = (0, pdf_codec.loadMathFont)();
209
+ for (const frame of block.frames ?? []) {
210
+ if (pageOfFrame(state, frame) === void 0) continue;
211
+ const sizePt = require_layout_shared.formulaSizePtForFrame(formula.mathml, frame, metricsAt);
212
+ const { box } = require_mathml_layout.layoutFormula(formula.mathml, {
213
+ metrics: metricsAt(sizePt),
214
+ sizePt,
215
+ color: document_schema_js.COLOR_BLACK
216
+ });
217
+ state.formulas.push({
218
+ pageIndex: frame.pageIndex,
219
+ xPt: frame.xPt,
220
+ yPt: frame.yPt,
221
+ box
222
+ });
223
+ }
169
224
  }
170
225
  function layoutDocumentFromPackage(pkg) {
226
+ return packageToLayout(pkg).document;
227
+ }
228
+ function packageToLayout(pkg) {
171
229
  const pages = (pkg.pages ?? []).map((page) => ({
172
230
  widthPt: page.widthPt,
173
231
  heightPt: page.heightPt,
174
232
  items: []
175
233
  }));
234
+ const fonts = (0, pdf_codec.createFontRegistry)({});
176
235
  const state = {
177
236
  pages,
178
- images: {}
237
+ images: {},
238
+ measurer: (0, pdf_codec.createFontMeasurer)(fonts),
239
+ fonts,
240
+ formulas: []
179
241
  };
180
242
  const content = (0, document_schema_js.flattenTree)(pkg);
181
243
  if (content.kind === "wordprocessing") for (const section of content.sections) emitBlocks(state, section.blocks);
@@ -186,10 +248,14 @@ function layoutDocumentFromPackage(pkg) {
186
248
  for (const shape of drawPage.shapes) emitBlocks(state, shape.blocks);
187
249
  }
188
250
  return {
189
- formatVersion: pdf_codec.LAYOUT_FORMAT_VERSION,
190
- metadata: content.metadata,
191
- pages,
192
- images: state.images
251
+ document: {
252
+ formatVersion: pdf_codec.LAYOUT_FORMAT_VERSION,
253
+ metadata: content.metadata,
254
+ pages,
255
+ images: state.images
256
+ },
257
+ formulas: state.formulas,
258
+ fonts: state.fonts
193
259
  };
194
260
  }
195
261
  //#endregion
@@ -1,17 +1,24 @@
1
1
  import { flipY } from "../model/geometry.js";
2
2
  import { requireArrayBufferBytes } from "../model/bytes.js";
3
- import { pushCellBorderLines, registerImage, runFont } from "../layout/shared.js";
3
+ import { formulaOfBlock } from "../model/formula.js";
4
+ import { layoutFormula } from "../mathml/layout.js";
5
+ import { atomizeForWrap, firstWrappedLineOf } from "../layout/text-layout.js";
6
+ import { formulaSizePtForFrame, pushCellBorderLines, registerImage, runFont } from "../layout/shared.js";
4
7
  import "../layout/sheets.js";
5
8
  import { convertVector } from "../layout/drawing.js";
6
9
  import { READ_ONLY_FORMATS } from "./capability.js";
7
10
  import { DOCUMENT_FORMAT_CODECS } from "../codecs/registry.js";
8
11
  import { COLOR_BLACK, DEFAULT_LAYOUT_FONT, flattenTree, resolveCellFillColor } from "document-schema.js";
9
- import { LAYOUT_FORMAT_VERSION, writePdf } from "pdf-codec";
12
+ import { LAYOUT_FORMAT_VERSION, createFontMeasurer, createFontRegistry, loadMathFont, writePdf } from "pdf-codec";
10
13
  //#region src/convert/from-package.ts
11
14
  function buildDocumentBytes(pkg, target) {
12
15
  if (target === "pdf") {
13
16
  if (pkg.pages === void 0) throw new Error("this DocumentTree has no pages -- only a package dumped from a <format>-to-pdf or pdf-to-<format> conversion carries them; a bridge conversion's own dump (e.g. odt-to-docx) never does, so 'pdf' is not a reachable target from it");
14
- return writePdf(layoutDocumentFromPackage(pkg));
17
+ const { document: layout, formulas, fonts } = packageToLayout(pkg);
18
+ return writePdf(layout, formulas.length > 0 ? {
19
+ formulas,
20
+ fonts
21
+ } : { fonts });
15
22
  }
16
23
  if (READ_ONLY_FORMATS.has(target)) throw new Error(`'${target}' is a read-only format: it cannot be built from a DocumentTree, because there is no ContentDocument-to-${target} builder`);
17
24
  const content = DOCUMENT_FORMAT_CODECS[target].content;
@@ -22,35 +29,62 @@ function pageOfFrame(state, frame) {
22
29
  return state.pages[frame.pageIndex];
23
30
  }
24
31
  function emitRun(state, run) {
25
- const frame = run.frames?.[0];
26
- if (frame === void 0) return;
27
- const page = pageOfFrame(state, frame);
28
- if (page === void 0) return;
32
+ const frames = run.frames ?? [];
29
33
  const font = runFont(run);
30
34
  const sizePt = run.sizePt ?? 18;
31
- const textItem = {
32
- kind: "text",
33
- text: run.text,
34
- xPt: frame.xPt,
35
- yPt: frame.yPt,
36
- font,
37
- sizePt,
38
- color: run.color ?? COLOR_BLACK,
39
- underline: run.underline
40
- };
41
- page.items.push(textItem);
42
- if (run.hyperlink !== void 0) {
43
- const link = {
35
+ const color = run.color ?? COLOR_BLACK;
36
+ const placements = frames.filter((frame) => pageOfFrame(state, frame) !== void 0);
37
+ if (placements.length === 0) return;
38
+ const fragments = placements.length === 1 ? [run.text] : rederiveWrapFragments(run.text, font, sizePt, placements, state.measurer);
39
+ for (const [index, frame] of placements.entries()) {
40
+ const page = pageOfFrame(state, frame);
41
+ if (page === void 0) continue;
42
+ const text = index === placements.length - 1 ? fragments.slice(index).join(" ") : fragments[index];
43
+ if (text !== void 0 && text !== "") page.items.push({
44
+ kind: "text",
45
+ text,
46
+ xPt: frame.xPt,
47
+ yPt: frame.yPt,
48
+ font,
49
+ sizePt,
50
+ color,
51
+ underline: run.underline
52
+ });
53
+ if (run.hyperlink !== void 0) page.items.push({
44
54
  kind: "link",
45
55
  uri: run.hyperlink,
46
56
  xPt: frame.xPt,
47
57
  yPt: frame.yPt,
48
58
  widthPt: frame.widthPt,
49
59
  heightPt: frame.heightPt
50
- };
51
- page.items.push(link);
60
+ });
52
61
  }
53
62
  }
63
+ function rederiveWrapFragments(text, font, sizePt, frames, measurer) {
64
+ let atoms = atomizeForWrap([{
65
+ text,
66
+ font,
67
+ sizePt,
68
+ color: COLOR_BLACK
69
+ }], measurer);
70
+ let remaining = text;
71
+ const fragments = [];
72
+ for (const frame of frames) {
73
+ if (remaining === "" || atoms.length === 0) break;
74
+ const { line, rest } = firstWrappedLineOf(atoms, measurer, frame.widthPt);
75
+ const consumed = line.fragments.map((f) => f.text).join("");
76
+ if (!remaining.startsWith(consumed)) return [text];
77
+ remaining = remaining.slice(consumed.length).trimStart();
78
+ fragments.push(consumed.trimEnd());
79
+ atoms = trimLeadingGlueAtoms(rest);
80
+ }
81
+ return fragments;
82
+ }
83
+ function trimLeadingGlueAtoms(atoms) {
84
+ let start = 0;
85
+ while (start < atoms.length && atoms[start]?.kind === "glue") start++;
86
+ return atoms.slice(start);
87
+ }
54
88
  function emitParagraph(state, paragraph) {
55
89
  for (const run of paragraph.runs) emitRun(state, run);
56
90
  }
@@ -165,16 +199,44 @@ function emitBlocks(state, blocks) {
165
199
  for (const block of blocks) if (block.kind === "paragraph") emitParagraph(state, block);
166
200
  else if (block.kind === "image") emitImageBlock(state, block, block.frames);
167
201
  else if (block.kind === "table") emitTable(state, block);
202
+ else if (block.kind === "embeddedObject") emitEmbeddedObjectBlock(state, block);
203
+ }
204
+ function emitEmbeddedObjectBlock(state, block) {
205
+ const formula = formulaOfBlock(block);
206
+ if (formula === void 0 || formula.mathml.length === 0) return;
207
+ const { metricsAt } = loadMathFont();
208
+ for (const frame of block.frames ?? []) {
209
+ if (pageOfFrame(state, frame) === void 0) continue;
210
+ const sizePt = formulaSizePtForFrame(formula.mathml, frame, metricsAt);
211
+ const { box } = layoutFormula(formula.mathml, {
212
+ metrics: metricsAt(sizePt),
213
+ sizePt,
214
+ color: COLOR_BLACK
215
+ });
216
+ state.formulas.push({
217
+ pageIndex: frame.pageIndex,
218
+ xPt: frame.xPt,
219
+ yPt: frame.yPt,
220
+ box
221
+ });
222
+ }
168
223
  }
169
224
  function layoutDocumentFromPackage(pkg) {
225
+ return packageToLayout(pkg).document;
226
+ }
227
+ function packageToLayout(pkg) {
170
228
  const pages = (pkg.pages ?? []).map((page) => ({
171
229
  widthPt: page.widthPt,
172
230
  heightPt: page.heightPt,
173
231
  items: []
174
232
  }));
233
+ const fonts = createFontRegistry({});
175
234
  const state = {
176
235
  pages,
177
- images: {}
236
+ images: {},
237
+ measurer: createFontMeasurer(fonts),
238
+ fonts,
239
+ formulas: []
178
240
  };
179
241
  const content = flattenTree(pkg);
180
242
  if (content.kind === "wordprocessing") for (const section of content.sections) emitBlocks(state, section.blocks);
@@ -185,10 +247,14 @@ function layoutDocumentFromPackage(pkg) {
185
247
  for (const shape of drawPage.shapes) emitBlocks(state, shape.blocks);
186
248
  }
187
249
  return {
188
- formatVersion: LAYOUT_FORMAT_VERSION,
189
- metadata: content.metadata,
190
- pages,
191
- images: state.images
250
+ document: {
251
+ formatVersion: LAYOUT_FORMAT_VERSION,
252
+ metadata: content.metadata,
253
+ pages,
254
+ images: state.images
255
+ },
256
+ formulas: state.formulas,
257
+ fonts: state.fonts
192
258
  };
193
259
  }
194
260
  //#endregion
@@ -158,6 +158,68 @@ function buildEmptyLine(runs, measurer) {
158
158
  descentPt: measurer.descenderAtSize(first.font, first.sizePt)
159
159
  };
160
160
  }
161
+ function atomizeForWrap(runs, measurer) {
162
+ return atomizeRuns(runs, measurer);
163
+ }
164
+ function firstWrappedLineOf(atoms, measurer, maxWidthPt, options = {}) {
165
+ const breakLongWords = options.breakLongWords ?? true;
166
+ const queue = [...atoms];
167
+ if (maxWidthPt <= 0) {
168
+ const all = queue.filter((a) => a.kind !== "break");
169
+ return {
170
+ line: all.length === 0 ? buildEmptyLine(dummyRunsOf(atoms), measurer) : buildLine(all, measurer),
171
+ rest: []
172
+ };
173
+ }
174
+ const current = [];
175
+ let currentWidth = 0;
176
+ while (queue.length > 0) {
177
+ const atom = queue[0];
178
+ if (atom.kind === "break") {
179
+ queue.shift();
180
+ break;
181
+ }
182
+ if (currentWidth + atom.widthPt <= maxWidthPt) {
183
+ current.push(atom);
184
+ currentWidth += atom.widthPt;
185
+ queue.shift();
186
+ continue;
187
+ }
188
+ if (current.length > 0) break;
189
+ if (atom.kind === "box" && breakLongWords) {
190
+ const { fit, rest } = splitBoxToWidth(atom, measurer, maxWidthPt);
191
+ current.push(fit);
192
+ currentWidth += fit.widthPt;
193
+ if (rest !== void 0) queue[0] = rest;
194
+ else queue.shift();
195
+ break;
196
+ }
197
+ current.push(atom);
198
+ currentWidth += atom.widthPt;
199
+ queue.shift();
200
+ break;
201
+ }
202
+ while (current.length > 0 && current[current.length - 1]?.kind === "glue") {
203
+ const removed = current.pop();
204
+ currentWidth -= removed?.widthPt ?? 0;
205
+ }
206
+ return {
207
+ line: current.length === 0 ? buildEmptyLine(dummyRunsOf(atoms), measurer) : buildLine(current, measurer),
208
+ rest: queue
209
+ };
210
+ }
211
+ function dummyRunsOf(atoms) {
212
+ for (const atom of atoms) if (atom.kind === "box" && atom.fragments[0] !== void 0) {
213
+ const f = atom.fragments[0];
214
+ return [{
215
+ text: f.text,
216
+ font: f.font,
217
+ sizePt: f.sizePt,
218
+ color: f.color
219
+ }];
220
+ }
221
+ return [];
222
+ }
161
223
  function wrapRunsToWidth(runs, measurer, maxWidthPt, options = {}) {
162
224
  const breakLongWords = options.breakLongWords ?? true;
163
225
  if (maxWidthPt <= 0) {
@@ -213,4 +275,6 @@ function wrapRunsToWidth(runs, measurer, maxWidthPt, options = {}) {
213
275
  return lines;
214
276
  }
215
277
  //#endregion
278
+ exports.atomizeForWrap = atomizeForWrap;
279
+ exports.firstWrappedLineOf = firstWrappedLineOf;
216
280
  exports.wrapRunsToWidth = wrapRunsToWidth;
@@ -11,6 +11,25 @@ interface SourcedWrappedLine extends Omit<WrappedLine, "fragments"> {
11
11
  readonly xOffsetPt: number;
12
12
  })[];
13
13
  }
14
+ interface BoxAtom {
15
+ readonly kind: "box";
16
+ readonly fragments: readonly SourcedFragment[];
17
+ readonly widthPt: number;
18
+ }
19
+ interface GlueAtom {
20
+ readonly kind: "glue";
21
+ readonly widthPt: number;
22
+ }
23
+ interface BreakAtom {
24
+ readonly kind: "break";
25
+ readonly widthPt: 0;
26
+ }
27
+ type WrapAtom = BoxAtom | GlueAtom | BreakAtom;
28
+ declare function atomizeForWrap(runs: readonly SourcedRun[], measurer: TextMeasurer): WrapAtom[];
29
+ declare function firstWrappedLineOf(atoms: readonly WrapAtom[], measurer: TextMeasurer, maxWidthPt: number, options?: WrapOptions): {
30
+ line: SourcedWrappedLine;
31
+ rest: WrapAtom[];
32
+ };
14
33
  declare function wrapRunsToWidth(runs: readonly SourcedRun[], measurer: TextMeasurer, maxWidthPt: number, options?: WrapOptions): SourcedWrappedLine[];
15
34
  //#endregion
16
- export { SourcedFragment, SourcedRun, SourcedWrappedLine, wrapRunsToWidth };
35
+ export { SourcedFragment, SourcedRun, SourcedWrappedLine, WrapAtom, atomizeForWrap, firstWrappedLineOf, wrapRunsToWidth };
@@ -11,6 +11,25 @@ interface SourcedWrappedLine extends Omit<WrappedLine, "fragments"> {
11
11
  readonly xOffsetPt: number;
12
12
  })[];
13
13
  }
14
+ interface BoxAtom {
15
+ readonly kind: "box";
16
+ readonly fragments: readonly SourcedFragment[];
17
+ readonly widthPt: number;
18
+ }
19
+ interface GlueAtom {
20
+ readonly kind: "glue";
21
+ readonly widthPt: number;
22
+ }
23
+ interface BreakAtom {
24
+ readonly kind: "break";
25
+ readonly widthPt: 0;
26
+ }
27
+ type WrapAtom = BoxAtom | GlueAtom | BreakAtom;
28
+ declare function atomizeForWrap(runs: readonly SourcedRun[], measurer: TextMeasurer): WrapAtom[];
29
+ declare function firstWrappedLineOf(atoms: readonly WrapAtom[], measurer: TextMeasurer, maxWidthPt: number, options?: WrapOptions): {
30
+ line: SourcedWrappedLine;
31
+ rest: WrapAtom[];
32
+ };
14
33
  declare function wrapRunsToWidth(runs: readonly SourcedRun[], measurer: TextMeasurer, maxWidthPt: number, options?: WrapOptions): SourcedWrappedLine[];
15
34
  //#endregion
16
- export { SourcedFragment, SourcedRun, SourcedWrappedLine, wrapRunsToWidth };
35
+ export { SourcedFragment, SourcedRun, SourcedWrappedLine, WrapAtom, atomizeForWrap, firstWrappedLineOf, wrapRunsToWidth };
@@ -157,6 +157,68 @@ function buildEmptyLine(runs, measurer) {
157
157
  descentPt: measurer.descenderAtSize(first.font, first.sizePt)
158
158
  };
159
159
  }
160
+ function atomizeForWrap(runs, measurer) {
161
+ return atomizeRuns(runs, measurer);
162
+ }
163
+ function firstWrappedLineOf(atoms, measurer, maxWidthPt, options = {}) {
164
+ const breakLongWords = options.breakLongWords ?? true;
165
+ const queue = [...atoms];
166
+ if (maxWidthPt <= 0) {
167
+ const all = queue.filter((a) => a.kind !== "break");
168
+ return {
169
+ line: all.length === 0 ? buildEmptyLine(dummyRunsOf(atoms), measurer) : buildLine(all, measurer),
170
+ rest: []
171
+ };
172
+ }
173
+ const current = [];
174
+ let currentWidth = 0;
175
+ while (queue.length > 0) {
176
+ const atom = queue[0];
177
+ if (atom.kind === "break") {
178
+ queue.shift();
179
+ break;
180
+ }
181
+ if (currentWidth + atom.widthPt <= maxWidthPt) {
182
+ current.push(atom);
183
+ currentWidth += atom.widthPt;
184
+ queue.shift();
185
+ continue;
186
+ }
187
+ if (current.length > 0) break;
188
+ if (atom.kind === "box" && breakLongWords) {
189
+ const { fit, rest } = splitBoxToWidth(atom, measurer, maxWidthPt);
190
+ current.push(fit);
191
+ currentWidth += fit.widthPt;
192
+ if (rest !== void 0) queue[0] = rest;
193
+ else queue.shift();
194
+ break;
195
+ }
196
+ current.push(atom);
197
+ currentWidth += atom.widthPt;
198
+ queue.shift();
199
+ break;
200
+ }
201
+ while (current.length > 0 && current[current.length - 1]?.kind === "glue") {
202
+ const removed = current.pop();
203
+ currentWidth -= removed?.widthPt ?? 0;
204
+ }
205
+ return {
206
+ line: current.length === 0 ? buildEmptyLine(dummyRunsOf(atoms), measurer) : buildLine(current, measurer),
207
+ rest: queue
208
+ };
209
+ }
210
+ function dummyRunsOf(atoms) {
211
+ for (const atom of atoms) if (atom.kind === "box" && atom.fragments[0] !== void 0) {
212
+ const f = atom.fragments[0];
213
+ return [{
214
+ text: f.text,
215
+ font: f.font,
216
+ sizePt: f.sizePt,
217
+ color: f.color
218
+ }];
219
+ }
220
+ return [];
221
+ }
160
222
  function wrapRunsToWidth(runs, measurer, maxWidthPt, options = {}) {
161
223
  const breakLongWords = options.breakLongWords ?? true;
162
224
  if (maxWidthPt <= 0) {
@@ -212,4 +274,4 @@ function wrapRunsToWidth(runs, measurer, maxWidthPt, options = {}) {
212
274
  return lines;
213
275
  }
214
276
  //#endregion
215
- export { wrapRunsToWidth };
277
+ export { atomizeForWrap, firstWrappedLineOf, wrapRunsToWidth };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js.documents",
3
- "version": "7.15.2",
3
+ "version": "7.16.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": {