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 +1 -1
- package/dist/convert/from-package.cjs +91 -25
- package/dist/convert/from-package.js +93 -27
- package/dist/layout/text-layout.cjs +64 -0
- package/dist/layout/text-layout.d.cts +20 -1
- package/dist/layout/text-layout.d.ts +20 -1
- package/dist/layout/text-layout.js +63 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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