cvdl-ts 1.0.25 → 1.0.27

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.
@@ -28,11 +28,13 @@ export type RenderProps = {
28
28
  bindings: Map<string, unknown>;
29
29
  storage: Storage;
30
30
  fontDict?: FontDict;
31
+ incremental?: boolean;
31
32
  };
33
+ export declare function resetIncrementalCaches(): void;
32
34
  export declare class FontDict {
33
35
  fonts: Map<string, fontkit.Font>;
34
36
  constructor();
35
37
  load_fonts(storage: Storage): Promise<this>;
36
38
  get_font(name: string): fontkit.Font;
37
39
  }
38
- export declare function render({ resume, layout_schemas, data_schemas, resume_layout, bindings, fontDict, }: RenderProps): Layout.RenderedLayout[];
40
+ export declare function render({ resume, layout_schemas, data_schemas, resume_layout, bindings, fontDict, incremental, }: RenderProps): Layout.RenderedLayout[];
package/dist/AnyLayout.js CHANGED
@@ -24,10 +24,50 @@ var __importStar = (this && this.__importStar) || function (mod) {
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.FontDict = void 0;
27
+ exports.resetIncrementalCaches = resetIncrementalCaches;
27
28
  exports.render = render;
28
29
  const ResumeLayout_1 = require("./ResumeLayout");
29
30
  const fontkit = __importStar(require("fontkit"));
30
31
  const Layout = __importStar(require("./Layout"));
32
+ const blockCache = new Map();
33
+ let flowPlacementCache = {
34
+ order: [],
35
+ signatures: [],
36
+ offsets: [],
37
+ heights: [],
38
+ };
39
+ const stableBindingsSignature = (bindings) => {
40
+ const sortedEntries = Array.from(bindings.entries()).sort(([a], [b]) => a.localeCompare(b));
41
+ return JSON.stringify(sortedEntries);
42
+ };
43
+ const stableSignature = (value) => {
44
+ try {
45
+ return JSON.stringify(value);
46
+ }
47
+ catch {
48
+ return String(value);
49
+ }
50
+ };
51
+ const getOrComputeBlock = (key, signature, compute, stats) => {
52
+ const cached = blockCache.get(key);
53
+ if (cached && cached.signature === signature) {
54
+ stats.hits += 1;
55
+ return cached.layout;
56
+ }
57
+ const layout = compute();
58
+ blockCache.set(key, { signature, layout });
59
+ stats.misses += 1;
60
+ return layout;
61
+ };
62
+ function resetIncrementalCaches() {
63
+ blockCache.clear();
64
+ flowPlacementCache = {
65
+ order: [],
66
+ signatures: [],
67
+ offsets: [],
68
+ heights: [],
69
+ };
70
+ }
31
71
  const cartesian = (...a) => a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat())));
32
72
  class FontDict {
33
73
  constructor() {
@@ -56,7 +96,8 @@ class FontDict {
56
96
  }
57
97
  }
58
98
  exports.FontDict = FontDict;
59
- function render({ resume, layout_schemas, data_schemas, resume_layout, bindings, fontDict, }) {
99
+ function render({ resume, layout_schemas, data_schemas, resume_layout, bindings, fontDict, incremental = true, }) {
100
+ var _a;
60
101
  // Compute the total usable width by subtracting the margins from the document width
61
102
  const width = resume_layout.width -
62
103
  (resume_layout.margin.left + resume_layout.margin.right);
@@ -65,6 +106,14 @@ function render({ resume, layout_schemas, data_schemas, resume_layout, bindings,
65
106
  ? width
66
107
  : width - (0, ResumeLayout_1.vertical_margin)(resume_layout.column_type) / 2.0;
67
108
  const layouts = [];
109
+ const usedKeys = new Set();
110
+ const stats = { hits: 0, misses: 0 };
111
+ const blockOrder = [];
112
+ const blockSignatures = [];
113
+ const blockHeights = [];
114
+ const bindingsSignature = stableBindingsSignature(bindings);
115
+ const fontSignature = stableSignature(Array.from(fontDict.fonts.keys()).sort());
116
+ let firstDirtyBlock = incremental ? -1 : 0;
68
117
  console.info("Rendering sections...");
69
118
  for (const section of resume.sections) {
70
119
  // Render Section Header
@@ -85,8 +134,33 @@ function render({ resume, layout_schemas, data_schemas, resume_layout, bindings,
85
134
  }
86
135
  start_time = Date.now();
87
136
  // 3. Render the header
88
- const layout = Layout.computeBoxes(Layout.normalize(Layout.instantiate(layout_schema.header_layout_schema, section.data, data_schema.header_schema, bindings), column_width, fontDict), fontDict);
137
+ const headerKey = `${section.section_name}::header`;
138
+ const headerSignature = stableSignature({
139
+ column_width,
140
+ fontSignature,
141
+ bindingsSignature,
142
+ layout: layout_schema.header_layout_schema,
143
+ fields: data_schema.header_schema,
144
+ data: section.data,
145
+ });
146
+ usedKeys.add(headerKey);
147
+ const blockIndex = blockOrder.length;
148
+ blockOrder.push(headerKey);
149
+ blockSignatures.push(headerSignature);
150
+ if (incremental &&
151
+ firstDirtyBlock === -1 &&
152
+ (flowPlacementCache.order[blockIndex] !== headerKey ||
153
+ flowPlacementCache.signatures[blockIndex] !== headerSignature)) {
154
+ firstDirtyBlock = blockIndex;
155
+ }
156
+ const layout = incremental
157
+ ? getOrComputeBlock(headerKey, headerSignature, () => Layout.computeBoxes(Layout.normalize(Layout.instantiate(layout_schema.header_layout_schema, section.data, data_schema.header_schema, bindings), column_width, fontDict), fontDict), stats)
158
+ : (() => {
159
+ stats.misses += 1;
160
+ return Layout.computeBoxes(Layout.normalize(Layout.instantiate(layout_schema.header_layout_schema, section.data, data_schema.header_schema, bindings), column_width, fontDict), fontDict);
161
+ })();
89
162
  layout.path = { tag: "section", section: section.section_name };
163
+ blockHeights.push(layout.bounding_box.height() + layout.margin.top + layout.margin.bottom);
90
164
  console.info("Header is computed");
91
165
  layouts.push(layout);
92
166
  end_time = Date.now();
@@ -94,14 +168,84 @@ function render({ resume, layout_schemas, data_schemas, resume_layout, bindings,
94
168
  start_time = Date.now();
95
169
  // Render Section Items
96
170
  for (const [index, item] of section.items.entries()) {
97
- // 3. Render the item
98
- const layout = Layout.computeBoxes(Layout.normalize(Layout.instantiate(layout_schema.item_layout_schema, item, data_schema.item_schema, bindings), column_width, fontDict), fontDict);
171
+ const itemKey = `${section.section_name}::item::${(_a = item.id) !== null && _a !== void 0 ? _a : index}`;
172
+ const itemSignature = stableSignature({
173
+ column_width,
174
+ fontSignature,
175
+ bindingsSignature,
176
+ layout: layout_schema.item_layout_schema,
177
+ fields: data_schema.item_schema,
178
+ data: item,
179
+ });
180
+ usedKeys.add(itemKey);
181
+ const blockIndex = blockOrder.length;
182
+ blockOrder.push(itemKey);
183
+ blockSignatures.push(itemSignature);
184
+ if (incremental &&
185
+ firstDirtyBlock === -1 &&
186
+ (flowPlacementCache.order[blockIndex] !== itemKey ||
187
+ flowPlacementCache.signatures[blockIndex] !== itemSignature)) {
188
+ firstDirtyBlock = blockIndex;
189
+ }
190
+ const layout = incremental
191
+ ? getOrComputeBlock(itemKey, itemSignature, () => Layout.computeBoxes(Layout.normalize(Layout.instantiate(layout_schema.item_layout_schema, item, data_schema.item_schema, bindings), column_width, fontDict), fontDict), stats)
192
+ : (() => {
193
+ stats.misses += 1;
194
+ return Layout.computeBoxes(Layout.normalize(Layout.instantiate(layout_schema.item_layout_schema, item, data_schema.item_schema, bindings), column_width, fontDict), fontDict);
195
+ })();
99
196
  layout.path = { tag: "item", section: section.section_name, item: index };
197
+ blockHeights.push(layout.bounding_box.height() + layout.margin.top + layout.margin.bottom);
100
198
  layouts.push(layout);
101
199
  }
102
200
  end_time = Date.now();
103
201
  console.info(`Item rendering time: ${end_time - start_time}ms for section ${section.section_name}`);
104
202
  }
203
+ const hasSameBlockShape = flowPlacementCache.order.length === blockOrder.length &&
204
+ firstDirtyBlock === -1;
205
+ if (!incremental || !hasSameBlockShape) {
206
+ if (firstDirtyBlock === -1) {
207
+ firstDirtyBlock = Math.min(blockOrder.length, flowPlacementCache.order.length);
208
+ }
209
+ }
210
+ const offsets = new Array(blockOrder.length).fill(0);
211
+ if (incremental && firstDirtyBlock > 0) {
212
+ for (let i = 0; i < firstDirtyBlock; i++) {
213
+ offsets[i] = flowPlacementCache.offsets[i];
214
+ }
215
+ }
216
+ if (!incremental || firstDirtyBlock !== -1) {
217
+ const start = !incremental ? 0 : Math.max(0, firstDirtyBlock);
218
+ let cursor = start === 0 ? 0 : offsets[start - 1] + blockHeights[start - 1];
219
+ for (let i = start; i < blockOrder.length; i++) {
220
+ offsets[i] = cursor;
221
+ cursor += blockHeights[i];
222
+ }
223
+ }
224
+ else {
225
+ for (let i = 0; i < blockOrder.length; i++) {
226
+ offsets[i] = flowPlacementCache.offsets[i];
227
+ }
228
+ }
229
+ for (let i = 0; i < layouts.length; i++) {
230
+ layouts[i].flow_offset_y = offsets[i];
231
+ }
232
+ if (incremental) {
233
+ for (const key of Array.from(blockCache.keys())) {
234
+ if (!usedKeys.has(key)) {
235
+ blockCache.delete(key);
236
+ }
237
+ }
238
+ flowPlacementCache = {
239
+ order: blockOrder,
240
+ signatures: blockSignatures,
241
+ offsets,
242
+ heights: blockHeights,
243
+ };
244
+ console.info(`Incremental block cache: ${stats.hits} hit(s), ${stats.misses} miss(es), firstDirtyBlock=${firstDirtyBlock}, size=${blockCache.size}`);
245
+ }
246
+ else {
247
+ console.info(`Full render mode: ${stats.misses} block(s) recomputed`);
248
+ }
105
249
  console.log("Position calculations are completed.");
106
250
  return layouts;
107
251
  }
package/dist/Elem.js CHANGED
@@ -354,12 +354,13 @@ function bind(t, bindings) {
354
354
  return result;
355
355
  }
356
356
  function instantiate(e, section, fields, bindings) {
357
+ var _a;
357
358
  e = bind(e, bindings);
358
359
  if (!e.is_ref) {
359
360
  return e;
360
361
  }
361
362
  const itemType = fields.find((f) => f.name === e.item);
362
- if (itemType.type.tag === "MarkdownString") {
363
+ if (((_a = itemType === null || itemType === void 0 ? void 0 : itemType.type) === null || _a === void 0 ? void 0 : _a.tag) === "MarkdownString") {
363
364
  e.is_markdown = true;
364
365
  }
365
366
  const text = section.fields[e.item];
package/dist/Layout.d.ts CHANGED
@@ -43,6 +43,7 @@ export type RenderedElem = Elem.t & {
43
43
  };
44
44
  export type RenderedLayout = (RenderedStack | RenderedRow | RenderedElem) & {
45
45
  path?: ElementPath;
46
+ flow_offset_y?: number;
46
47
  };
47
48
  export declare function default_(tag: string): Stack.t | Row.t | Elem.t;
48
49
  export declare function empty(): Layout;
package/dist/Layout.js CHANGED
@@ -261,25 +261,27 @@ function computeTextboxPositions(l, top_left, font_dict) {
261
261
  top_left = top_left.move_y_by(row.margin.top).move_x_by(row.margin.left);
262
262
  const originalTopLeft = top_left;
263
263
  let per_elem_space = 0.0;
264
+ const rowElementsWidth = Row.elementsWidth(row);
264
265
  switch (row.alignment) {
265
266
  case "Center":
266
- top_left = top_left.move_x_by((Width.get_fixed_unchecked(row.width) - Row.elementsWidth(row)) /
267
- 2.0);
267
+ top_left = top_left.move_x_by((Width.get_fixed_unchecked(row.width) - rowElementsWidth) / 2.0);
268
268
  break;
269
269
  case "Right":
270
- top_left = top_left.move_x_by(Width.get_fixed_unchecked(row.width) - Row.elementsWidth(row));
270
+ top_left = top_left.move_x_by(Width.get_fixed_unchecked(row.width) - rowElementsWidth);
271
271
  break;
272
272
  case "Justified":
273
- per_elem_space =
274
- (Width.get_fixed_unchecked(row.width) - Row.elementsWidth(row)) /
275
- (row.elements.length - 1);
273
+ if (row.elements.length > 1) {
274
+ per_elem_space =
275
+ (Width.get_fixed_unchecked(row.width) - rowElementsWidth) /
276
+ (row.elements.length - 1);
277
+ }
276
278
  break;
277
279
  }
278
280
  const renderedElements = [];
279
281
  for (const element of row.elements) {
280
282
  const result = computeTextboxPositions(element, top_left, font_dict);
281
283
  depth = Math.max(depth, result.depth);
282
- top_left = top_left.move_x_by(Width.get_fixed_unchecked(element.width) + per_elem_space);
284
+ top_left = top_left.move_x_by(Row.elementOuterWidth(element) + per_elem_space);
283
285
  renderedElements.push(result.renderedLayout);
284
286
  }
285
287
  depth += row.margin.bottom;
@@ -304,15 +306,16 @@ function computeTextboxPositions(l, top_left, font_dict) {
304
306
  .move_y_by(elem.margin.top)
305
307
  .move_x_by(elem.margin.left);
306
308
  let line = 1;
307
- let cursor = top_left.x;
309
+ let cursor = 0;
310
+ const maxLineWidth = Width.get_fixed_unchecked(elem.width) - elem.margin.right;
311
+ const wrapTolerance = 1e-6;
308
312
  elem.spans.forEach((span) => {
309
- if (cursor - top_left.x + span.width >
310
- Width.get_fixed_unchecked(elem.width) - elem.margin.right ||
313
+ if (cursor + span.width > maxLineWidth + wrapTolerance ||
311
314
  span.text === "\n\n") {
312
- cursor = top_left.x;
315
+ cursor = 0;
313
316
  line += 1;
314
317
  }
315
- span.bbox = new Box_1.Box(new Point_1.Point(cursor - top_left.x, (line - 1) * height), new Point_1.Point(cursor + span.width, line * height));
318
+ span.bbox = new Box_1.Box(new Point_1.Point(cursor, (line - 1) * height), new Point_1.Point(cursor + span.width, line * height));
316
319
  span.line = line;
317
320
  cursor += span.width;
318
321
  });
package/dist/PdfLayout.js CHANGED
@@ -32,7 +32,9 @@ const AnyLayout_1 = require("./AnyLayout");
32
32
  const pdfkit_1 = __importDefault(require("pdfkit"));
33
33
  const Resume = __importStar(require("./Resume"));
34
34
  const _1 = require(".");
35
+ const Box_1 = require("./Box");
35
36
  const render = async ({ resume_name, resume, data_schemas, layout_schemas, resume_layout, bindings, storage, fontDict, }) => {
37
+ var _a;
36
38
  let start_time = Date.now();
37
39
  if (!resume && !resume_name) {
38
40
  throw "Rendering requires either resume_name or resume";
@@ -95,9 +97,12 @@ const render = async ({ resume_name, resume, data_schemas, layout_schemas, resum
95
97
  fontDict: fontDict,
96
98
  };
97
99
  for (const layout of layouts) {
98
- (0, exports.renderSectionLayout)(layout, tracker);
99
- tracker.height +=
100
- layout.bounding_box.height() + layout.margin.top + layout.margin.bottom;
100
+ const flowOffset = (_a = layout.flow_offset_y) !== null && _a !== void 0 ? _a : tracker.height;
101
+ (0, exports.renderSectionLayout)(layout, { ...tracker, height: flowOffset });
102
+ if (layout.flow_offset_y === undefined) {
103
+ tracker.height +=
104
+ layout.bounding_box.height() + layout.margin.top + layout.margin.bottom;
105
+ }
101
106
  }
102
107
  console.log("Rendering is completed. Saving the document...");
103
108
  console.log("Document is saved to output.pdf");
@@ -122,20 +127,38 @@ const getPageContainer = (page, tracker) => {
122
127
  return tracker.pageContainer;
123
128
  };
124
129
  const mergeSpans = (spans) => {
130
+ if (spans.length === 0) {
131
+ return [];
132
+ }
133
+ const firstSpan = spans[0];
134
+ if (!firstSpan) {
135
+ return [];
136
+ }
125
137
  const merged_spans = [];
126
- let currentSpan = spans[0];
138
+ let currentSpan = firstSpan;
127
139
  for (let i = 1; i < spans.length; i++) {
128
- if (currentSpan.bbox.top_left.y === spans[i].bbox.top_left.y &&
129
- currentSpan.font === spans[i].font &&
130
- currentSpan.is_code === spans[i].is_code &&
131
- currentSpan.is_bold === spans[i].is_bold &&
132
- currentSpan.is_italic === spans[i].is_italic) {
133
- currentSpan.text += spans[i].text;
134
- currentSpan.bbox.bottom_right = spans[i].bbox.bottom_right;
140
+ const nextSpan = spans[i];
141
+ if (!nextSpan) {
142
+ continue;
143
+ }
144
+ const hasBothBBoxes = Boolean(currentSpan.bbox && nextSpan.bbox);
145
+ if (hasBothBBoxes &&
146
+ currentSpan.bbox.top_left.y === nextSpan.bbox.top_left.y &&
147
+ currentSpan.font === nextSpan.font &&
148
+ currentSpan.is_code === nextSpan.is_code &&
149
+ currentSpan.is_bold === nextSpan.is_bold &&
150
+ currentSpan.is_italic === nextSpan.is_italic) {
151
+ currentSpan = {
152
+ ...currentSpan,
153
+ text: currentSpan.text + nextSpan.text,
154
+ bbox: currentSpan.bbox && nextSpan.bbox
155
+ ? new Box_1.Box(currentSpan.bbox.top_left, nextSpan.bbox.bottom_right)
156
+ : currentSpan.bbox,
157
+ };
135
158
  }
136
159
  else {
137
160
  merged_spans.push(currentSpan);
138
- currentSpan = spans[i];
161
+ currentSpan = nextSpan;
139
162
  }
140
163
  }
141
164
  merged_spans.push(currentSpan);
package/dist/Row.d.ts CHANGED
@@ -17,6 +17,7 @@ export declare function from(w: Optional<Row>): Row;
17
17
  export declare function row(elements: Layout.t[], margin: Margin.t, alignment: Alignment.t, width: Width.t, is_frozen: boolean, is_fill: boolean): Row;
18
18
  export declare function default_(): Row;
19
19
  export declare function elementsWidth(r: Row): number;
20
+ export declare function elementOuterWidth(e: Layout.t): number;
20
21
  export declare function boundWidth(r: Row, width: number): Row;
21
22
  export declare function scaleWidth(r: Row, w: number): Row;
22
23
  export {};
package/dist/Row.js CHANGED
@@ -27,6 +27,7 @@ exports.from = from;
27
27
  exports.row = row;
28
28
  exports.default_ = default_;
29
29
  exports.elementsWidth = elementsWidth;
30
+ exports.elementOuterWidth = elementOuterWidth;
30
31
  exports.boundWidth = boundWidth;
31
32
  exports.scaleWidth = scaleWidth;
32
33
  const Margin = __importStar(require("./Margin"));
@@ -60,9 +61,10 @@ function default_() {
60
61
  };
61
62
  }
62
63
  function elementsWidth(r) {
63
- return r.elements
64
- .map((e) => Width.get_fixed_unchecked(e.width))
65
- .reduce((a, b) => a + b, 0.0);
64
+ return r.elements.map(elementOuterWidth).reduce((a, b) => a + b, 0.0);
65
+ }
66
+ function elementOuterWidth(e) {
67
+ return e.margin.left + Width.get_fixed_unchecked(e.width) + e.margin.right;
66
68
  }
67
69
  function boundWidth(r, width) {
68
70
  const bound = r.width.tag === "Absolute"
@@ -73,7 +75,32 @@ function boundWidth(r, width) {
73
75
  if (bound === null) {
74
76
  throw new Error("Cannot bound width of non-unitized widths!");
75
77
  }
76
- return row(r.elements.map((e) => Layout.boundWidth(e, bound)), r.margin, r.alignment, Width.absolute(bound), r.is_frozen, Width.is_fill(r.width));
78
+ const fixedWidths = r.elements.map((e) => {
79
+ switch (e.width.tag) {
80
+ case "Fill":
81
+ return null;
82
+ case "Absolute":
83
+ return Math.min(e.width.value, bound);
84
+ case "Percent":
85
+ // Row children are usually scaled before bounding, but this keeps
86
+ // direct boundWidth() calls stable if Percent widths appear.
87
+ return Math.min((e.width.value * bound) / 100.0, bound);
88
+ }
89
+ });
90
+ const totalFixedOuterWidth = fixedWidths.reduce((acc, next, index) => next === null
91
+ ? acc
92
+ : acc + r.elements[index].margin.left + next + r.elements[index].margin.right, 0);
93
+ const fillElementCount = fixedWidths.filter((next) => next === null).length;
94
+ const totalFillMargins = fixedWidths.reduce((acc, next, index) => next === null
95
+ ? acc +
96
+ r.elements[index].margin.left +
97
+ r.elements[index].margin.right
98
+ : acc, 0);
99
+ const fillWidth = fillElementCount > 0
100
+ ? Math.max(0, bound - totalFixedOuterWidth - totalFillMargins) /
101
+ fillElementCount
102
+ : 0;
103
+ return row(r.elements.map((e, index) => { var _a; return Layout.boundWidth(e, (_a = fixedWidths[index]) !== null && _a !== void 0 ? _a : fillWidth); }), r.margin, r.alignment, Width.absolute(bound), r.is_frozen, Width.is_fill(r.width));
77
104
  }
78
105
  function scaleWidth(r, w) {
79
106
  return (0, Utils_1.with_)(r, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cvdl-ts",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "Typescript Implementation of CVDL Compiler",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,6 +24,8 @@
24
24
  "scripts": {
25
25
  "build": "tsc",
26
26
  "start": "ts-node src/index.ts",
27
- "cli": "ts-node src/cli.ts"
27
+ "cli": "ts-node src/cli.ts",
28
+ "bench:incremental": "npm run build && node scripts/benchmark_incremental.cjs",
29
+ "bench:incremental:full": "npm run build && BENCH_PROFILE=full node scripts/benchmark_incremental.cjs"
28
30
  }
29
31
  }