postext 0.3.6 → 0.3.8

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.
Files changed (137) hide show
  1. package/dist/__tests__/exports.test.js +3 -0
  2. package/dist/__tests__/exports.test.js.map +1 -1
  3. package/dist/canvas-backend/blockRender.d.ts +3 -0
  4. package/dist/canvas-backend/blockRender.d.ts.map +1 -0
  5. package/dist/canvas-backend/blockRender.js +176 -0
  6. package/dist/canvas-backend/blockRender.js.map +1 -0
  7. package/dist/canvas-backend/decorations.d.ts +6 -0
  8. package/dist/canvas-backend/decorations.d.ts.map +1 -0
  9. package/dist/canvas-backend/decorations.js +106 -0
  10. package/dist/canvas-backend/decorations.js.map +1 -0
  11. package/dist/{canvas-backend.d.ts → canvas-backend/index.d.ts} +2 -2
  12. package/dist/canvas-backend/index.d.ts.map +1 -0
  13. package/dist/canvas-backend/index.js +66 -0
  14. package/dist/canvas-backend/index.js.map +1 -0
  15. package/dist/defaults/index.d.ts +1 -0
  16. package/dist/defaults/index.d.ts.map +1 -1
  17. package/dist/defaults/index.js +9 -0
  18. package/dist/defaults/index.js.map +1 -1
  19. package/dist/defaults/pdfGeneration.d.ts +5 -0
  20. package/dist/defaults/pdfGeneration.d.ts.map +1 -0
  21. package/dist/defaults/pdfGeneration.js +37 -0
  22. package/dist/defaults/pdfGeneration.js.map +1 -0
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/knuthPlass/breakpoints.d.ts +6 -0
  28. package/dist/knuthPlass/breakpoints.d.ts.map +1 -0
  29. package/dist/knuthPlass/breakpoints.js +284 -0
  30. package/dist/knuthPlass/breakpoints.js.map +1 -0
  31. package/dist/knuthPlass/constants.d.ts +8 -0
  32. package/dist/knuthPlass/constants.d.ts.map +1 -0
  33. package/dist/knuthPlass/constants.js +8 -0
  34. package/dist/knuthPlass/constants.js.map +1 -0
  35. package/dist/knuthPlass/index.d.ts +12 -0
  36. package/dist/knuthPlass/index.d.ts.map +1 -0
  37. package/dist/knuthPlass/index.js +11 -0
  38. package/dist/knuthPlass/index.js.map +1 -0
  39. package/dist/knuthPlass/pretextAdapter.d.ts +12 -0
  40. package/dist/knuthPlass/pretextAdapter.d.ts.map +1 -0
  41. package/dist/knuthPlass/pretextAdapter.js +178 -0
  42. package/dist/knuthPlass/pretextAdapter.js.map +1 -0
  43. package/dist/knuthPlass/richAdapter.d.ts +26 -0
  44. package/dist/knuthPlass/richAdapter.d.ts.map +1 -0
  45. package/dist/knuthPlass/richAdapter.js +189 -0
  46. package/dist/knuthPlass/richAdapter.js.map +1 -0
  47. package/dist/{knuthPlass.d.ts → knuthPlass/types.d.ts} +1 -31
  48. package/dist/knuthPlass/types.d.ts.map +1 -0
  49. package/dist/knuthPlass/types.js +5 -0
  50. package/dist/knuthPlass/types.js.map +1 -0
  51. package/dist/knuthPlass/utils.d.ts +2 -0
  52. package/dist/knuthPlass/utils.d.ts.map +1 -0
  53. package/dist/knuthPlass/utils.js +4 -0
  54. package/dist/knuthPlass/utils.js.map +1 -0
  55. package/dist/measure/cache.d.ts +5 -0
  56. package/dist/measure/cache.d.ts.map +1 -0
  57. package/dist/measure/cache.js +38 -0
  58. package/dist/measure/cache.js.map +1 -0
  59. package/dist/measure/canvas.d.ts +8 -0
  60. package/dist/measure/canvas.d.ts.map +1 -0
  61. package/dist/measure/canvas.js +30 -0
  62. package/dist/measure/canvas.js.map +1 -0
  63. package/dist/measure/font.d.ts +19 -0
  64. package/dist/measure/font.d.ts.map +1 -0
  65. package/dist/measure/font.js +34 -0
  66. package/dist/measure/font.js.map +1 -0
  67. package/dist/measure/index.d.ts +7 -0
  68. package/dist/measure/index.d.ts.map +1 -0
  69. package/dist/measure/index.js +6 -0
  70. package/dist/measure/index.js.map +1 -0
  71. package/dist/measure/plain.d.ts +13 -0
  72. package/dist/measure/plain.d.ts.map +1 -0
  73. package/dist/measure/plain.js +167 -0
  74. package/dist/measure/plain.js.map +1 -0
  75. package/dist/measure/rich.d.ts +25 -0
  76. package/dist/measure/rich.d.ts.map +1 -0
  77. package/dist/measure/rich.js +246 -0
  78. package/dist/measure/rich.js.map +1 -0
  79. package/dist/measure/types.d.ts +28 -0
  80. package/dist/measure/types.d.ts.map +1 -0
  81. package/dist/measure/types.js +2 -0
  82. package/dist/measure/types.js.map +1 -0
  83. package/dist/parse/blockParser.d.ts +28 -0
  84. package/dist/parse/blockParser.d.ts.map +1 -0
  85. package/dist/parse/blockParser.js +302 -0
  86. package/dist/parse/blockParser.js.map +1 -0
  87. package/dist/parse/index.d.ts +4 -0
  88. package/dist/parse/index.d.ts.map +1 -0
  89. package/dist/parse/index.js +3 -0
  90. package/dist/parse/index.js.map +1 -0
  91. package/dist/parse/inlineFormatting.d.ts +12 -0
  92. package/dist/parse/inlineFormatting.d.ts.map +1 -0
  93. package/dist/parse/inlineFormatting.js +90 -0
  94. package/dist/parse/inlineFormatting.js.map +1 -0
  95. package/dist/parse/inlineMath.d.ts +29 -0
  96. package/dist/parse/inlineMath.d.ts.map +1 -0
  97. package/dist/parse/inlineMath.js +141 -0
  98. package/dist/parse/inlineMath.js.map +1 -0
  99. package/dist/parse/sourceMapping.d.ts +12 -0
  100. package/dist/parse/sourceMapping.d.ts.map +1 -0
  101. package/dist/parse/sourceMapping.js +130 -0
  102. package/dist/parse/sourceMapping.js.map +1 -0
  103. package/dist/{parse.d.ts → parse/types.d.ts} +2 -25
  104. package/dist/parse/types.d.ts.map +1 -0
  105. package/dist/parse/types.js +2 -0
  106. package/dist/parse/types.js.map +1 -0
  107. package/dist/pipeline/build.d.ts.map +1 -1
  108. package/dist/pipeline/build.js +34 -276
  109. package/dist/pipeline/build.js.map +1 -1
  110. package/dist/pipeline/buildBlockKind.d.ts +33 -0
  111. package/dist/pipeline/buildBlockKind.d.ts.map +1 -0
  112. package/dist/pipeline/buildBlockKind.js +82 -0
  113. package/dist/pipeline/buildBlockKind.js.map +1 -0
  114. package/dist/pipeline/buildHelpers.d.ts +47 -0
  115. package/dist/pipeline/buildHelpers.d.ts.map +1 -0
  116. package/dist/pipeline/buildHelpers.js +169 -0
  117. package/dist/pipeline/buildHelpers.js.map +1 -0
  118. package/dist/pipeline/buildMeasurement.d.ts +27 -0
  119. package/dist/pipeline/buildMeasurement.d.ts.map +1 -0
  120. package/dist/pipeline/buildMeasurement.js +49 -0
  121. package/dist/pipeline/buildMeasurement.js.map +1 -0
  122. package/dist/types.d.ts +15 -0
  123. package/dist/types.d.ts.map +1 -1
  124. package/package.json +1 -1
  125. package/dist/canvas-backend.d.ts.map +0 -1
  126. package/dist/canvas-backend.js +0 -370
  127. package/dist/canvas-backend.js.map +0 -1
  128. package/dist/knuthPlass.d.ts.map +0 -1
  129. package/dist/knuthPlass.js +0 -665
  130. package/dist/knuthPlass.js.map +0 -1
  131. package/dist/measure.d.ts +0 -61
  132. package/dist/measure.d.ts.map +0 -1
  133. package/dist/measure.js +0 -512
  134. package/dist/measure.js.map +0 -1
  135. package/dist/parse.d.ts.map +0 -1
  136. package/dist/parse.js +0 -653
  137. package/dist/parse.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sourceMapping.d.ts","sourceRoot":"","sources":["../../src/parse/sourceMapping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AA+F1C;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,UAAU,EAAE,GACrB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CA0B5D"}
@@ -0,0 +1,130 @@
1
+ import { MATH_PLACEHOLDER } from './inlineMath';
2
+ /**
3
+ * Build a per-character map from plain text to absolute source offsets.
4
+ * Greedy matches each plain char against the raw source (delimited by
5
+ * [blockSrcStart, blockSrcEnd)), skipping markdown markers and treating
6
+ * newlines/tabs as spaces for paragraph line joins.
7
+ */
8
+ function computeSourceMap(markdown, blockSrcStart, blockSrcEnd, plainText) {
9
+ const map = new Array(plainText.length);
10
+ let r = blockSrcStart;
11
+ for (let p = 0; p < plainText.length; p++) {
12
+ const ch = plainText[p];
13
+ // Math placeholder: the plain char represents `$...$` in the markdown.
14
+ // Advance to the opening `$`, map to it, then skip past the closing `$`
15
+ // so subsequent plain chars can keep aligning with the source.
16
+ if (ch === MATH_PLACEHOLDER) {
17
+ while (r < blockSrcEnd && markdown[r] !== '$')
18
+ r++;
19
+ if (r >= blockSrcEnd) {
20
+ map[p] = blockSrcEnd;
21
+ continue;
22
+ }
23
+ map[p] = r;
24
+ let j = r + 1;
25
+ while (j < blockSrcEnd) {
26
+ if (markdown[j] === '\\' && markdown[j + 1] === '$') {
27
+ j += 2;
28
+ continue;
29
+ }
30
+ if (markdown[j] === '$') {
31
+ j++;
32
+ break;
33
+ }
34
+ if (markdown[j] === '\n')
35
+ break;
36
+ j++;
37
+ }
38
+ r = j;
39
+ continue;
40
+ }
41
+ const isSpace = ch === ' ';
42
+ while (r < blockSrcEnd) {
43
+ const rc = markdown[r];
44
+ if (rc === ch)
45
+ break;
46
+ if (isSpace && (rc === '\n' || rc === '\t'))
47
+ break;
48
+ r++;
49
+ }
50
+ if (r >= blockSrcEnd) {
51
+ map[p] = blockSrcEnd;
52
+ }
53
+ else {
54
+ map[p] = r;
55
+ r++;
56
+ }
57
+ }
58
+ return map;
59
+ }
60
+ const COLLAPSIBLE_WS_RE = /[ \t\n\r\f]/;
61
+ /**
62
+ * Collapse runs of `[ \t\n\r\f]` to a single space and strip leading/trailing
63
+ * whitespace across spans. Mirrors pretext's `normalizeWhitespaceNormal` so
64
+ * that `spans` and the block's plain text stay aligned with what the layout
65
+ * engine actually renders — otherwise cursor/selection mapping drifts by one
66
+ * character per collapsed whitespace character.
67
+ */
68
+ function normalizeWhitespaceInSpans(spans) {
69
+ const out = [];
70
+ let inSpace = true; // start true to strip leading whitespace
71
+ for (const span of spans) {
72
+ let result = '';
73
+ for (const ch of span.text) {
74
+ if (COLLAPSIBLE_WS_RE.test(ch)) {
75
+ if (!inSpace) {
76
+ result += ' ';
77
+ inSpace = true;
78
+ }
79
+ }
80
+ else {
81
+ result += ch;
82
+ inSpace = false;
83
+ }
84
+ }
85
+ out.push({ ...span, text: result });
86
+ }
87
+ // Strip trailing space from the last span that contributed content
88
+ for (let i = out.length - 1; i >= 0; i--) {
89
+ const text = out[i].text;
90
+ if (text.length === 0)
91
+ continue;
92
+ if (text.endsWith(' ')) {
93
+ out[i] = { ...out[i], text: text.slice(0, -1) };
94
+ }
95
+ break;
96
+ }
97
+ return out.filter((s) => s.text.length > 0);
98
+ }
99
+ /**
100
+ * Build normalized text, spans, and sourceMap for a block. The plain text is
101
+ * whitespace-normalized to match pretext's internal normalization so that the
102
+ * per-character `sourceMap` aligns with rendered line segments.
103
+ */
104
+ export function buildBlockMapping(markdown, blockSrcStart, blockSrcEnd, rawSpans) {
105
+ const rawText = rawSpans.map((s) => s.text).join('');
106
+ const rawSourceMap = computeSourceMap(markdown, blockSrcStart, blockSrcEnd, rawText);
107
+ const spans = normalizeWhitespaceInSpans(rawSpans);
108
+ const text = spans.map((s) => s.text).join('');
109
+ // Walk the raw text building the same normalization, and project each kept
110
+ // normalized character onto the rawSourceMap to obtain the source offset.
111
+ const sourceMap = [];
112
+ let inSpace = true;
113
+ for (let i = 0; i < rawText.length; i++) {
114
+ const ch = rawText[i];
115
+ if (COLLAPSIBLE_WS_RE.test(ch)) {
116
+ if (!inSpace) {
117
+ sourceMap.push(rawSourceMap[i] ?? blockSrcEnd);
118
+ inSpace = true;
119
+ }
120
+ }
121
+ else {
122
+ sourceMap.push(rawSourceMap[i] ?? blockSrcEnd);
123
+ inSpace = false;
124
+ }
125
+ }
126
+ if (sourceMap.length > text.length)
127
+ sourceMap.length = text.length;
128
+ return { text, spans, sourceMap };
129
+ }
130
+ //# sourceMappingURL=sourceMapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sourceMapping.js","sourceRoot":"","sources":["../../src/parse/sourceMapping.ts"],"names":[],"mappings":"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"}
@@ -18,7 +18,7 @@ export interface InlineSpan {
18
18
  math?: MathMeta;
19
19
  /** Resolved math render — populated by the pipeline before measurement
20
20
  * so the parser remains free of MathJax dependencies. */
21
- mathRender?: import('./math/types').MathRender;
21
+ mathRender?: import('../math/types').MathRender;
22
22
  }
23
23
  /** Convenience discriminants for inline span iteration. */
24
24
  export type TextSpan = InlineSpan & {
@@ -66,27 +66,4 @@ export interface ContentBlock {
66
66
  */
67
67
  sourceMap: number[];
68
68
  }
69
- /** Object Replacement Character — atomic plain-text placeholder for a math
70
- * span. One code unit per formula so `sourceMap` stays 1-to-1. */
71
- export declare const MATH_PLACEHOLDER = "\uFFFC";
72
- /**
73
- * Memoized wrapper around parseMarkdown: returns the cached result when the
74
- * input string is byte-for-byte identical to the previous call. This avoids
75
- * reparsing the whole document on each keystroke when upstream recomputes
76
- * only because a sibling state changed.
77
- */
78
- export declare function parseMarkdownMemo(markdown: string): ContentBlock[];
79
- export declare function parseMarkdownWithIssuesMemo(markdown: string): {
80
- blocks: ContentBlock[];
81
- issues: ParseIssue[];
82
- };
83
- export declare function parseMarkdown(markdown: string): ContentBlock[];
84
- /**
85
- * Merge consecutive blockquote lines into a single block, and consecutive
86
- * non-blank, non-special lines into paragraphs.
87
- */
88
- export declare function parseMarkdownWithIssues(markdown: string): {
89
- blocks: ContentBlock[];
90
- issues: ParseIssue[];
91
- };
92
- //# sourceMappingURL=parse.d.ts.map
69
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/parse/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,WAAW,GACX,YAAY,GACZ,UAAU,GACV,aAAa,CAAC;AAElB;;sEAEsE;AACtE,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB;uEACmE;IACnE,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB;8DAC0D;IAC1D,UAAU,CAAC,EAAE,OAAO,eAAe,EAAE,UAAU,CAAC;CACjD;AAED,2DAA2D;AAC3D,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,CAAC;AACzD,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEvD,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;AAExD,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,mBAAmB,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,GAAG,GAAG,IAAI,CAAC;IACtB,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB;mCAC+B;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2CAA2C;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/parse/types.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/pipeline/build.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9D,OAAO,EAIL,KAAK,WAAW,EAEjB,MAAM,QAAQ,CAAC;AAKhB,OAAO,KAAK,EAAE,gBAAgB,EAAiB,MAAM,YAAY,CAAC;AAuBlE,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC;CAC9B;AAED,qBAAa,mBAAoB,SAAQ,KAAK;;CAK7C;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,MAAM,CAAC,EAAE,aAAa,EACtB,KAAK,CAAC,EAAE,gBAAgB,EACxB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,WAAW,CAmtBb"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/pipeline/build.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9D,OAAO,EAGL,KAAK,WAAW,EAEjB,MAAM,QAAQ,CAAC;AAKhB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA4BnD,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC;CAC9B;AAED,qBAAa,mBAAoB,SAAQ,KAAK;;CAK7C;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,MAAM,CAAC,EAAE,aAAa,EACtB,KAAK,CAAC,EAAE,gBAAgB,EACxB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,WAAW,CAifb"}
@@ -1,15 +1,17 @@
1
1
  import { dimensionToPx } from '../units';
2
- import { createBoundingBox, createVDTDocument, createVDTBlock, } from '../vdt';
2
+ import { createVDTDocument, createVDTBlock, } from '../vdt';
3
3
  import { parseMarkdownMemo } from '../parse';
4
4
  import { computeHeadingNumbers } from '../numbering';
5
5
  import { extractFrontmatter } from '../frontmatter';
6
- import { measureBlock, measureRichBlock, cachedMeasureBlock, cachedMeasureRichBlock, initHyphenator } from '../measure';
6
+ import { initHyphenator } from '../measure';
7
7
  import { resolveAllConfig, computeBaselineGrid } from './config';
8
- import { resolveBodyStyle, resolveHeadingStyle, resolveBlockquoteStyle, resolveMathDisplayStyle } from './styles';
9
- import { renderMath, isMathReady } from '../math';
10
- import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, resolveUnorderedListItemStyle, resolveOrderedListItemStyle, } from './lists';
8
+ import { resolveBodyStyle, resolveBlockquoteStyle } from './styles';
9
+ import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, } from './lists';
11
10
  import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, placeBlockInColumn, } from './placement';
12
11
  import { chooseParagraphSplit } from './orphanWidow';
12
+ import { applyStyleAttrs, computeMeasureViewport, computePageMetrics, enrichMathSpans, rollbackTrailingBlocks, stampSourceRanges, } from './buildHelpers';
13
+ import { resolveBlockKind } from './buildBlockKind';
14
+ import { runMeasurement } from './buildMeasurement';
13
15
  export class BuildCancelledError extends Error {
14
16
  constructor() {
15
17
  super('Build cancelled');
@@ -27,26 +29,7 @@ export function buildDocument(content, config, cache, options) {
27
29
  const baselineGrid = computeBaselineGrid(resolved);
28
30
  // Create document
29
31
  const doc = createVDTDocument(resolved, baselineGrid);
30
- // Page dimensions in px (trim size)
31
- const trimWidthPx = dimensionToPx(resolved.page.width, dpi);
32
- const trimHeightPx = dimensionToPx(resolved.page.height, dpi);
33
- // Cut lines expansion: canvas grows to fit bleed + mark offset + mark length
34
- let trimOffset = 0;
35
- if (resolved.page.cutLines.enabled) {
36
- const bleedPx = dimensionToPx(resolved.page.cutLines.bleed, dpi);
37
- const markOffsetPx = dimensionToPx(resolved.page.cutLines.markOffset, dpi);
38
- const markLengthPx = dimensionToPx(resolved.page.cutLines.markLength, dpi);
39
- trimOffset = bleedPx + markOffsetPx + markLengthPx;
40
- }
41
- const pageWidthPx = trimWidthPx + trimOffset * 2;
42
- const pageHeightPx = trimHeightPx + trimOffset * 2;
43
- // Margins in px
44
- const marginTop = dimensionToPx(resolved.page.margins.top, dpi);
45
- const marginBottom = dimensionToPx(resolved.page.margins.bottom, dpi);
46
- const marginLeft = dimensionToPx(resolved.page.margins.left, dpi);
47
- const marginRight = dimensionToPx(resolved.page.margins.right, dpi);
48
- // Content area (offset by trimOffset so content sits inside the trim area)
49
- const contentArea = createBoundingBox(marginLeft + trimOffset, marginTop + trimOffset, trimWidthPx - marginLeft - marginRight, trimHeightPx - marginTop - marginBottom);
32
+ const { pageWidthPx, pageHeightPx, trimOffset, contentArea } = computePageMetrics(resolved);
50
33
  doc.trimOffset = trimOffset;
51
34
  // Create first page
52
35
  const firstPage = createPageWithColumns(0, resolved, contentArea, pageWidthPx, pageHeightPx);
@@ -77,110 +60,26 @@ export function buildDocument(content, config, cache, options) {
77
60
  throw new BuildCancelledError();
78
61
  const rawBlock = contentBlocks[blockIdx];
79
62
  const id = `block-${blockIdCounter++}`;
80
- let style;
81
- let vdtType;
82
- let headingLevel;
83
- let numberPrefix;
84
- let contentBlock = rawBlock;
85
- let listBullet;
86
- let listDepth;
87
- let listKind;
88
- let bulletXOffsetInColumn = 0;
89
- let strikethroughText = false;
90
- switch (rawBlock.type) {
91
- case 'heading': {
92
- style = resolveHeadingStyle(rawBlock.level ?? 1, resolved);
93
- vdtType = 'heading';
94
- headingLevel = rawBlock.level;
95
- numberPrefix = headingPrefixes[blockIdx];
96
- if (numberPrefix) {
97
- const sep = `${numberPrefix} `;
98
- const firstSpan = rawBlock.spans[0];
99
- const newSpans = firstSpan
100
- ? [{ text: sep + firstSpan.text, bold: firstSpan.bold, italic: firstSpan.italic }, ...rawBlock.spans.slice(1)]
101
- : [{ text: sep, bold: false, italic: false }];
102
- contentBlock = { ...rawBlock, text: sep + rawBlock.text, spans: newSpans };
103
- }
104
- break;
105
- }
106
- case 'blockquote':
107
- style = blockquoteStyle;
108
- vdtType = 'blockquote';
109
- break;
110
- case 'mathDisplay': {
111
- style = resolveMathDisplayStyle(resolved);
112
- vdtType = 'mathDisplay';
113
- break;
114
- }
115
- case 'listItem': {
116
- const depth = rawBlock.depth ?? 1;
117
- const kind = rawBlock.listKind ?? 'unordered';
118
- let resolvedList;
119
- if (kind === 'ordered') {
120
- const metric = orderedMetrics.perBlock.get(blockIdx) ??
121
- { numberText: '', numberWidthPx: 0, maxNumberWidthPx: 0 };
122
- resolvedList = resolveOrderedListItemStyle(depth, resolved, orderedLevelIndentsPx, metric);
123
- }
124
- else {
125
- resolvedList = resolveUnorderedListItemStyle(depth, resolved, listLevelIndentsPx, rawBlock.checked ?? false, kind === 'task');
126
- }
127
- style = resolvedList.text;
128
- listBullet = resolvedList.bullet;
129
- listDepth = depth;
130
- listKind = kind;
131
- bulletXOffsetInColumn = resolvedList.bulletXOffsetInColumn;
132
- strikethroughText = resolvedList.strikethroughText;
133
- vdtType = 'listItem';
134
- break;
135
- }
136
- default:
137
- style = bodyStyle;
138
- vdtType = 'paragraph';
139
- break;
140
- }
63
+ const kind = resolveBlockKind(rawBlock, {
64
+ resolved,
65
+ bodyStyle,
66
+ blockquoteStyle,
67
+ headingPrefixes,
68
+ blockIdx,
69
+ listLevelIndentsPx,
70
+ orderedLevelIndentsPx,
71
+ orderedMetrics,
72
+ });
73
+ const { style, vdtType, headingLevel, numberPrefix, listBullet, listDepth, listKind, bulletXOffsetInColumn, strikethroughText } = kind;
74
+ let contentBlock = kind.contentBlock;
141
75
  // Measure text — use rich measurement for blocks with bold spans
142
76
  const col = currentColumn(doc, cursor);
143
- // Resolve inline math: renderMath each `span.math` and attach mathRender.
144
- // When math is disabled, drop the math metadata so spans fall back to
145
- // the raw TeX (visible as literal `$...$`).
77
+ // Resolve inline math on spans (no-op when the block has no math).
146
78
  const mathEnabled = resolved.math.enabled;
147
- if (contentBlock.spans.some((s) => s.math)) {
148
- const mathFontSizePx = style.fontSizePx * resolved.math.fontSizeScale;
149
- const mathColor = resolved.math.color?.hex ?? style.color;
150
- const enrichedSpans = contentBlock.spans.map((s) => {
151
- if (!s.math)
152
- return s;
153
- if (!mathEnabled) {
154
- return { text: `$${s.math.tex}$`, bold: s.bold, italic: s.italic };
155
- }
156
- const render = isMathReady()
157
- ? renderMath(s.math.tex, false, mathFontSizePx, { lineBoxPx: style.lineHeightPx, color: mathColor })
158
- : undefined;
159
- return { ...s, mathRender: render };
160
- });
161
- contentBlock = { ...contentBlock, spans: enrichedSpans };
162
- }
79
+ contentBlock = enrichMathSpans(contentBlock, style, resolved);
163
80
  const hasRichSpans = contentBlock.spans.some((s) => s.bold || s.italic || s.mathRender);
164
81
  // List items reserve horizontal space for indent + bullet + gap.
165
- let measureMaxWidth = col.bbox.width;
166
- let lineXShift = 0;
167
- let measureFirstLineIndent = style.firstLineIndentPx;
168
- let measureHangingIndent = style.hangingIndent;
169
- if (listBullet) {
170
- const textGap = listBullet.bulletWidthPx + listBullet.gapPx;
171
- if (listBullet.hangingIndent) {
172
- measureMaxWidth = Math.max(1, col.bbox.width - listBullet.indentPx - textGap);
173
- lineXShift = listBullet.indentPx + textGap;
174
- measureFirstLineIndent = 0;
175
- measureHangingIndent = false;
176
- }
177
- else {
178
- measureMaxWidth = Math.max(1, col.bbox.width - listBullet.indentPx);
179
- lineXShift = listBullet.indentPx;
180
- measureFirstLineIndent = textGap;
181
- measureHangingIndent = false;
182
- }
183
- }
82
+ const { measureMaxWidth, lineXShift, measureFirstLineIndent, measureHangingIndent, } = computeMeasureViewport(col.bbox.width, style, listBullet);
184
83
  const runtActive = resolved.bodyText.avoidRunts
185
84
  && (vdtType === 'paragraph'
186
85
  || (vdtType === 'listItem' && resolved.bodyText.avoidRuntsInLists));
@@ -195,50 +94,10 @@ export function buildDocument(content, config, cache, options) {
195
94
  runtPenalty: runtActive ? resolved.bodyText.runtPenalty : 0,
196
95
  runtMinCharacters: runtActive ? resolved.bodyText.runtMinCharacters : 0,
197
96
  };
198
- const useRich = hasRichSpans && style.boldFontString && style.italicFontString && style.boldItalicFontString;
199
- // Math display block: bypass text layout entirely. Produce a single
200
- // VDTLine whose bbox is the math render's pixel box, centred later.
201
- let mathDisplayRender;
202
- let measured;
203
- if (vdtType === 'mathDisplay') {
204
- const tex = rawBlock.tex ?? '';
205
- if (!mathEnabled) {
206
- // Fallback: render the literal TeX as a paragraph-like run.
207
- measured = useRich
208
- ? measureRichBlock([{ text: `$$${tex}$$`, bold: false, italic: false }], style.fontString, style.fontString, style.fontString, style.fontString, measureMaxWidth, style.lineHeightPx, measureOptions)
209
- : measureBlock(`$$${tex}$$`, style.fontString, measureMaxWidth, style.lineHeightPx, measureOptions);
210
- }
211
- else {
212
- // `renderMath` internally returns a cheap placeholder when MathJax
213
- // isn't initialised yet — no need to gate the call here. When the
214
- // real engine lands later, `CanvasPreview` bumps `resizeKey` and the
215
- // pipeline rebuilds with the genuine render.
216
- const render = renderMath(tex, true, style.fontSizePx, { color: style.color });
217
- mathDisplayRender = render;
218
- const width = Math.min(render.widthPx, measureMaxWidth);
219
- const height = render.heightPx;
220
- measured = {
221
- lines: [{
222
- text: '',
223
- bbox: { x: 0, y: 0, width, height },
224
- baseline: render.ascentPx,
225
- hyphenated: false,
226
- segments: [{ kind: 'math', text: '\uFFFC', width, mathRender: render }],
227
- isLastLine: true,
228
- }],
229
- totalHeight: height,
230
- };
231
- }
232
- }
233
- else {
234
- measured = cache
235
- ? (useRich
236
- ? cachedMeasureRichBlock(contentBlock.spans, style.fontString, style.boldFontString, style.italicFontString, style.boldItalicFontString, measureMaxWidth, style.lineHeightPx, measureOptions, cache)
237
- : cachedMeasureBlock(contentBlock.text, style.fontString, measureMaxWidth, style.lineHeightPx, measureOptions, cache))
238
- : (useRich
239
- ? measureRichBlock(contentBlock.spans, style.fontString, style.boldFontString, style.italicFontString, style.boldItalicFontString, measureMaxWidth, style.lineHeightPx, measureOptions)
240
- : measureBlock(contentBlock.text, style.fontString, measureMaxWidth, style.lineHeightPx, measureOptions));
241
- }
97
+ const useRich = !!(hasRichSpans && style.boldFontString && style.italicFontString && style.boldItalicFontString);
98
+ const { measured, mathDisplayRender } = runMeasurement({
99
+ vdtType, rawBlock, contentBlock, style, measureMaxWidth, measureOptions, mathEnabled, useRich, cache,
100
+ });
242
101
  if (measured.lines.length === 0)
243
102
  continue;
244
103
  if (lineXShift > 0) {
@@ -248,46 +107,7 @@ export function buildDocument(content, config, cache, options) {
248
107
  }
249
108
  // Per-line source-range mapping using the block's plain→source map.
250
109
  // Accounts for heading numbering prefix which prepends chars with no source.
251
- const blockSrcStart = rawBlock.sourceStart + bodyOffset;
252
- const blockSrcEnd = rawBlock.sourceEnd + bodyOffset;
253
- const srcMap = rawBlock.sourceMap;
254
- const prefixLen = contentBlock.text.length - rawBlock.text.length;
255
- const plainToSrc = (p) => {
256
- const idx = p - prefixLen;
257
- if (idx <= 0)
258
- return blockSrcStart;
259
- if (idx >= srcMap.length)
260
- return blockSrcEnd;
261
- return srcMap[idx] + bodyOffset;
262
- };
263
- let cumPlain = 0;
264
- const lastLineIdx = measured.lines.length - 1;
265
- for (let li = 0; li < measured.lines.length; li++) {
266
- const line = measured.lines[li];
267
- // If segments are present, prefer their aggregate text length for a more
268
- // accurate plain-char count (excludes trailing hyphen for hyphenated lines).
269
- let lineLen;
270
- if (line.segments && line.segments.length > 0) {
271
- lineLen = line.segments.reduce((s, seg) => s + seg.text.length, 0);
272
- if (line.hyphenated) {
273
- const last = line.segments[line.segments.length - 1];
274
- if (last.text.endsWith('-'))
275
- lineLen -= 1;
276
- }
277
- }
278
- else {
279
- lineLen = line.text.length - (line.hyphenated ? 1 : 0);
280
- }
281
- line.plainStart = cumPlain;
282
- line.plainEnd = cumPlain + lineLen;
283
- // Advance past the separator space that was consumed to break the line
284
- // (skip when hyphenated — break was at a soft hyphen — or on the last line).
285
- const skipSeparator = !line.hyphenated && li !== lastLineIdx ? 1 : 0;
286
- cumPlain = line.plainEnd + skipSeparator;
287
- line.sourceStart = plainToSrc(line.plainStart);
288
- line.sourceEnd = plainToSrc(line.plainEnd);
289
- }
290
- const absoluteSourceMap = srcMap.map((o) => o + bodyOffset);
110
+ const { prefixLen, absoluteSourceMap } = stampSourceRanges(measured, rawBlock, contentBlock, bodyOffset);
291
111
  const finalizeListItem = (blk, isFirstPart) => {
292
112
  if (!listBullet)
293
113
  return;
@@ -395,16 +215,7 @@ export function buildDocument(content, config, cache, options) {
395
215
  curCol.availableHeight -= spacingBefore;
396
216
  const splitLines = remainingLines.slice(0, splitAt);
397
217
  const blk = createVDTBlock(id, vdtType, style.fontString, style.color, style.textAlign);
398
- if (style.boldFontString)
399
- blk.boldFontString = style.boldFontString;
400
- if (style.italicFontString)
401
- blk.italicFontString = style.italicFontString;
402
- if (style.boldItalicFontString)
403
- blk.boldItalicFontString = style.boldItalicFontString;
404
- if (style.boldColor)
405
- blk.boldColor = style.boldColor;
406
- if (style.italicColor)
407
- blk.italicColor = style.italicColor;
218
+ applyStyleAttrs(blk, style);
408
219
  blk.headingLevel = headingLevel;
409
220
  if (numberPrefix)
410
221
  blk.numberPrefix = numberPrefix;
@@ -491,21 +302,8 @@ export function buildDocument(content, config, cache, options) {
491
302
  if (remainAfterHeading < minSpaceAfter) {
492
303
  // Roll back any immediately-preceding heading blocks in this
493
304
  // column so they travel with this one.
494
- let rollbackCount = 0;
495
- for (let j = curCol.blocks.length - 1; j >= 0; j--) {
496
- if (curCol.blocks[j].type === 'heading')
497
- rollbackCount++;
498
- else
499
- break;
500
- }
305
+ const rollbackCount = rollbackTrailingBlocks(curCol, doc.blocks, (b) => b.type === 'heading');
501
306
  if (rollbackCount > 0) {
502
- const popped = curCol.blocks.splice(curCol.blocks.length - rollbackCount);
503
- for (const p of popped) {
504
- const idx = doc.blocks.indexOf(p);
505
- if (idx !== -1)
506
- doc.blocks.splice(idx, 1);
507
- curCol.availableHeight += p.bbox.height;
508
- }
509
307
  // Rewind so the for-loop's blockIdx++ lands on the first
510
308
  // rolled-back heading.
511
309
  blockIdx -= rollbackCount + 1;
@@ -524,16 +322,7 @@ export function buildDocument(content, config, cache, options) {
524
322
  }
525
323
  const partId = partIndex === 0 ? id : `${id}-cont-${partIndex}`;
526
324
  const blk = createVDTBlock(partId, vdtType, style.fontString, style.color, style.textAlign);
527
- if (style.boldFontString)
528
- blk.boldFontString = style.boldFontString;
529
- if (style.italicFontString)
530
- blk.italicFontString = style.italicFontString;
531
- if (style.boldItalicFontString)
532
- blk.boldItalicFontString = style.boldItalicFontString;
533
- if (style.boldColor)
534
- blk.boldColor = style.boldColor;
535
- if (style.italicColor)
536
- blk.italicColor = style.italicColor;
325
+ applyStyleAttrs(blk, style);
537
326
  if (partIndex === 0) {
538
327
  blk.headingLevel = headingLevel;
539
328
  if (numberPrefix)
@@ -613,16 +402,7 @@ export function buildDocument(content, config, cache, options) {
613
402
  const partId = partIndex === 0 ? id : `${id}-cont-${partIndex}`;
614
403
  const splitLines = remainingLines.slice(0, choice.splitAt);
615
404
  const blk = createVDTBlock(partId, vdtType, style.fontString, style.color, style.textAlign);
616
- if (style.boldFontString)
617
- blk.boldFontString = style.boldFontString;
618
- if (style.italicFontString)
619
- blk.italicFontString = style.italicFontString;
620
- if (style.boldItalicFontString)
621
- blk.boldItalicFontString = style.boldItalicFontString;
622
- if (style.boldColor)
623
- blk.boldColor = style.boldColor;
624
- if (style.italicColor)
625
- blk.italicColor = style.italicColor;
405
+ applyStyleAttrs(blk, style);
626
406
  if (partIndex === 0) {
627
407
  blk.headingLevel = headingLevel;
628
408
  if (numberPrefix)
@@ -656,21 +436,8 @@ export function buildDocument(content, config, cache, options) {
656
436
  // pull those headings along so they don't remain stranded as orphans
657
437
  // at the column's bottom. Mirrors the rollback inside the "fits" path.
658
438
  if (vdtType === 'heading' && resolved.headings.keepWithNext) {
659
- let rollbackCount = 0;
660
- for (let j = curCol.blocks.length - 1; j >= 0; j--) {
661
- if (curCol.blocks[j].type === 'heading')
662
- rollbackCount++;
663
- else
664
- break;
665
- }
439
+ const rollbackCount = rollbackTrailingBlocks(curCol, doc.blocks, (b) => b.type === 'heading');
666
440
  if (rollbackCount > 0) {
667
- const popped = curCol.blocks.splice(curCol.blocks.length - rollbackCount);
668
- for (const p of popped) {
669
- const idx = doc.blocks.indexOf(p);
670
- if (idx !== -1)
671
- doc.blocks.splice(idx, 1);
672
- curCol.availableHeight += p.bbox.height;
673
- }
674
441
  blockIdx -= rollbackCount + 1;
675
442
  pendingSpacing = 0;
676
443
  advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx);
@@ -684,16 +451,7 @@ export function buildDocument(content, config, cache, options) {
684
451
  // Empty column but block still doesn't fit (block taller than page) — place anyway
685
452
  const partId = partIndex === 0 ? id : `${id}-cont-${partIndex}`;
686
453
  const blk = createVDTBlock(partId, vdtType, style.fontString, style.color, style.textAlign);
687
- if (style.boldFontString)
688
- blk.boldFontString = style.boldFontString;
689
- if (style.italicFontString)
690
- blk.italicFontString = style.italicFontString;
691
- if (style.boldItalicFontString)
692
- blk.boldItalicFontString = style.boldItalicFontString;
693
- if (style.boldColor)
694
- blk.boldColor = style.boldColor;
695
- if (style.italicColor)
696
- blk.italicColor = style.italicColor;
454
+ applyStyleAttrs(blk, style);
697
455
  if (partIndex === 0)
698
456
  blk.headingLevel = headingLevel;
699
457
  blk.lines = resetLinePositions(remainingLines, style.lineHeightPx);