@opengeni/artifact-tool 0.2.2

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 (203) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +104 -0
  3. package/dist/bounded-zip.d.ts +28 -0
  4. package/dist/chunk-4BSENMAE.js +39 -0
  5. package/dist/chunk-4BSENMAE.js.map +1 -0
  6. package/dist/chunk-4FRFNNBM.js +276 -0
  7. package/dist/chunk-4FRFNNBM.js.map +1 -0
  8. package/dist/chunk-5XIA6KHZ.js +49 -0
  9. package/dist/chunk-5XIA6KHZ.js.map +1 -0
  10. package/dist/chunk-66CT2ZRP.js +2229 -0
  11. package/dist/chunk-66CT2ZRP.js.map +1 -0
  12. package/dist/chunk-72SYDU4O.js +319 -0
  13. package/dist/chunk-72SYDU4O.js.map +1 -0
  14. package/dist/chunk-B2ZJRZDA.js +279 -0
  15. package/dist/chunk-B2ZJRZDA.js.map +1 -0
  16. package/dist/chunk-DCTLTNPW.js +2458 -0
  17. package/dist/chunk-DCTLTNPW.js.map +1 -0
  18. package/dist/chunk-FGZM4FQ6.js +473 -0
  19. package/dist/chunk-FGZM4FQ6.js.map +1 -0
  20. package/dist/chunk-HTJBLBDE.js +591 -0
  21. package/dist/chunk-HTJBLBDE.js.map +1 -0
  22. package/dist/chunk-IYFXZQ34.js +5306 -0
  23. package/dist/chunk-IYFXZQ34.js.map +1 -0
  24. package/dist/chunk-J6N7IR44.js +172 -0
  25. package/dist/chunk-J6N7IR44.js.map +1 -0
  26. package/dist/chunk-N2IHNWBS.js +1002 -0
  27. package/dist/chunk-N2IHNWBS.js.map +1 -0
  28. package/dist/chunk-NAG44K67.js +549 -0
  29. package/dist/chunk-NAG44K67.js.map +1 -0
  30. package/dist/chunk-O24KTEGP.js +33 -0
  31. package/dist/chunk-O24KTEGP.js.map +1 -0
  32. package/dist/chunk-PI2AZVGK.js +2074 -0
  33. package/dist/chunk-PI2AZVGK.js.map +1 -0
  34. package/dist/chunk-Q2FFHEY7.js +40 -0
  35. package/dist/chunk-Q2FFHEY7.js.map +1 -0
  36. package/dist/chunk-RBGQADXA.js +57 -0
  37. package/dist/chunk-RBGQADXA.js.map +1 -0
  38. package/dist/chunk-U5G2BKM2.js +613 -0
  39. package/dist/chunk-U5G2BKM2.js.map +1 -0
  40. package/dist/chunk-VOJHMSM3.js +39 -0
  41. package/dist/chunk-VOJHMSM3.js.map +1 -0
  42. package/dist/chunk-VUNV25KB.js +16 -0
  43. package/dist/chunk-VUNV25KB.js.map +1 -0
  44. package/dist/chunk-XIFUNVBO.js +101 -0
  45. package/dist/chunk-XIFUNVBO.js.map +1 -0
  46. package/dist/chunk-ZCJYRJGB.js +489 -0
  47. package/dist/chunk-ZCJYRJGB.js.map +1 -0
  48. package/dist/chunk-ZZXPEE4S.js +2260 -0
  49. package/dist/chunk-ZZXPEE4S.js.map +1 -0
  50. package/dist/document-docx-api.d.ts +49 -0
  51. package/dist/document-docx-codec.d.ts +9 -0
  52. package/dist/document-docx-codec.js +4765 -0
  53. package/dist/document-docx-codec.js.map +1 -0
  54. package/dist/document-docx-import.d.ts +261 -0
  55. package/dist/document-docx-packer.d.ts +18 -0
  56. package/dist/document-docx-state.d.ts +12 -0
  57. package/dist/document-render.d.ts +3 -0
  58. package/dist/document-render.js +492 -0
  59. package/dist/document-render.js.map +1 -0
  60. package/dist/document.d.ts +432 -0
  61. package/dist/document.js +37 -0
  62. package/dist/document.js.map +1 -0
  63. package/dist/errors.d.ts +23 -0
  64. package/dist/file-blob.d.ts +11 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.js +186 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/inspection.d.ts +12 -0
  69. package/dist/kernel.d.ts +101 -0
  70. package/dist/lib-GQLPACFK.js +9746 -0
  71. package/dist/lib-GQLPACFK.js.map +1 -0
  72. package/dist/materializer-cli-entry.d.ts +2 -0
  73. package/dist/materializer-cli-entry.js +29 -0
  74. package/dist/materializer-cli-entry.js.map +1 -0
  75. package/dist/materializer-cli.d.ts +24 -0
  76. package/dist/materializer-cli.js +24 -0
  77. package/dist/materializer-cli.js.map +1 -0
  78. package/dist/native-raster.d.ts +10 -0
  79. package/dist/native.d.ts +129 -0
  80. package/dist/native.js +31 -0
  81. package/dist/native.js.map +1 -0
  82. package/dist/office-import.d.ts +48 -0
  83. package/dist/office-import.js +154 -0
  84. package/dist/office-import.js.map +1 -0
  85. package/dist/presentation-pptx-api.d.ts +60 -0
  86. package/dist/presentation-pptx-import.d.ts +5 -0
  87. package/dist/presentation-pptx-state-digest-765CZZED.js +10 -0
  88. package/dist/presentation-pptx-state-digest-765CZZED.js.map +1 -0
  89. package/dist/presentation-pptx-state-digest.d.ts +3 -0
  90. package/dist/presentation-pptx-state.d.ts +4 -0
  91. package/dist/presentation-pptx-xml.d.ts +29 -0
  92. package/dist/presentation-pptx.d.ts +5 -0
  93. package/dist/presentation-pptx.js +2365 -0
  94. package/dist/presentation-pptx.js.map +1 -0
  95. package/dist/presentation-render.d.ts +29 -0
  96. package/dist/presentation-render.js +375 -0
  97. package/dist/presentation-render.js.map +1 -0
  98. package/dist/presentation.d.ts +565 -0
  99. package/dist/presentation.js +71 -0
  100. package/dist/presentation.js.map +1 -0
  101. package/dist/production-composite.d.ts +68 -0
  102. package/dist/production-document.d.ts +2 -0
  103. package/dist/production-document.js +176 -0
  104. package/dist/production-document.js.map +1 -0
  105. package/dist/production-native-codecs.d.ts +94 -0
  106. package/dist/production-presentation.d.ts +2 -0
  107. package/dist/production-presentation.js +176 -0
  108. package/dist/production-presentation.js.map +1 -0
  109. package/dist/production-sha256.d.ts +2 -0
  110. package/dist/production-spreadsheet-native.d.ts +12 -0
  111. package/dist/production-spreadsheet.d.ts +2 -0
  112. package/dist/production-spreadsheet.js +176 -0
  113. package/dist/production-spreadsheet.js.map +1 -0
  114. package/dist/production.d.ts +96 -0
  115. package/dist/raster-image.d.ts +34 -0
  116. package/dist/reference.d.ts +17 -0
  117. package/dist/reference.js +574 -0
  118. package/dist/reference.js.map +1 -0
  119. package/dist/runtime-cli-entry.d.ts +2 -0
  120. package/dist/runtime-cli-entry.js +21 -0
  121. package/dist/runtime-cli-entry.js.map +1 -0
  122. package/dist/runtime-cli.d.ts +76 -0
  123. package/dist/runtime-cli.js +26 -0
  124. package/dist/runtime-cli.js.map +1 -0
  125. package/dist/runtime-development.d.ts +73 -0
  126. package/dist/runtime-development.js +30 -0
  127. package/dist/runtime-development.js.map +1 -0
  128. package/dist/runtime-receipt.d.ts +19 -0
  129. package/dist/runtime.d.ts +355 -0
  130. package/dist/runtime.js +42 -0
  131. package/dist/runtime.js.map +1 -0
  132. package/dist/snapshot.d.ts +26 -0
  133. package/dist/snapshot.js +61 -0
  134. package/dist/snapshot.js.map +1 -0
  135. package/dist/spreadsheet-address.d.ts +18 -0
  136. package/dist/spreadsheet-file.d.ts +17 -0
  137. package/dist/spreadsheet-formula.d.ts +67 -0
  138. package/dist/spreadsheet-image.d.ts +17 -0
  139. package/dist/spreadsheet-render.d.ts +5 -0
  140. package/dist/spreadsheet-render.js +752 -0
  141. package/dist/spreadsheet-render.js.map +1 -0
  142. package/dist/spreadsheet-types.d.ts +155 -0
  143. package/dist/spreadsheet-xlsx-api.d.ts +88 -0
  144. package/dist/spreadsheet-xlsx-codec.d.ts +16 -0
  145. package/dist/spreadsheet-xlsx-codec.js +16 -0
  146. package/dist/spreadsheet-xlsx-codec.js.map +1 -0
  147. package/dist/spreadsheet-xlsx-state.d.ts +16 -0
  148. package/dist/spreadsheet.d.ts +724 -0
  149. package/dist/spreadsheet.js +67 -0
  150. package/dist/spreadsheet.js.map +1 -0
  151. package/package.json +129 -0
  152. package/src/bounded-zip.ts +450 -0
  153. package/src/document-docx-api.ts +64 -0
  154. package/src/document-docx-codec.ts +2253 -0
  155. package/src/document-docx-import.ts +3814 -0
  156. package/src/document-docx-packer.ts +72 -0
  157. package/src/document-docx-state.ts +59 -0
  158. package/src/document-render.ts +615 -0
  159. package/src/document.ts +2800 -0
  160. package/src/errors.ts +45 -0
  161. package/src/file-blob.ts +43 -0
  162. package/src/index.ts +4 -0
  163. package/src/inspection.ts +50 -0
  164. package/src/kernel.ts +525 -0
  165. package/src/materializer-cli-entry.ts +11 -0
  166. package/src/materializer-cli.ts +743 -0
  167. package/src/native-raster.ts +141 -0
  168. package/src/native.ts +390 -0
  169. package/src/office-import.ts +243 -0
  170. package/src/presentation-pptx-api.ts +107 -0
  171. package/src/presentation-pptx-import.ts +2065 -0
  172. package/src/presentation-pptx-state-digest.ts +46 -0
  173. package/src/presentation-pptx-state.ts +18 -0
  174. package/src/presentation-pptx-xml.ts +473 -0
  175. package/src/presentation-pptx.ts +447 -0
  176. package/src/presentation-render.ts +532 -0
  177. package/src/presentation.ts +2599 -0
  178. package/src/production-composite.ts +805 -0
  179. package/src/production-document.ts +2 -0
  180. package/src/production-native-codecs.ts +1141 -0
  181. package/src/production-presentation.ts +2 -0
  182. package/src/production-sha256.ts +150 -0
  183. package/src/production-spreadsheet-native.ts +686 -0
  184. package/src/production-spreadsheet.ts +2 -0
  185. package/src/production.ts +1022 -0
  186. package/src/raster-image.ts +586 -0
  187. package/src/reference.ts +17 -0
  188. package/src/runtime-cli-entry.ts +13 -0
  189. package/src/runtime-cli.ts +670 -0
  190. package/src/runtime-development.ts +689 -0
  191. package/src/runtime-receipt.ts +169 -0
  192. package/src/runtime.ts +1463 -0
  193. package/src/snapshot.ts +88 -0
  194. package/src/spreadsheet-address.ts +90 -0
  195. package/src/spreadsheet-file.ts +71 -0
  196. package/src/spreadsheet-formula.ts +1212 -0
  197. package/src/spreadsheet-image.ts +242 -0
  198. package/src/spreadsheet-render.ts +985 -0
  199. package/src/spreadsheet-types.ts +171 -0
  200. package/src/spreadsheet-xlsx-api.ts +123 -0
  201. package/src/spreadsheet-xlsx-codec.ts +3063 -0
  202. package/src/spreadsheet-xlsx-state.ts +191 -0
  203. package/src/spreadsheet.ts +5195 -0
@@ -0,0 +1,615 @@
1
+ import { ArtifactLimitError } from "./errors";
2
+ import { FileBlob } from "./file-blob";
3
+ import { rasterizeSvgToPng } from "./native-raster";
4
+ import {
5
+ Document,
6
+ DocumentPageBreak,
7
+ DocumentParagraph,
8
+ DocumentTable,
9
+ type DocumentBlock,
10
+ type DocumentCommentThread,
11
+ type DocumentRenderOptions,
12
+ type DocumentSection,
13
+ type DocumentStory,
14
+ type DocumentStoryBlock,
15
+ type DocumentTableStyle,
16
+ type DocumentTextRun,
17
+ type DocumentTextStyle,
18
+ type TrackedChange,
19
+ } from "./document";
20
+
21
+ const MAX_RASTER_DIMENSION = 32_768;
22
+ const MAX_RASTER_PIXELS = 128_000_000;
23
+ const MAX_RASTER_GLYPHS = 50_000;
24
+ const MAX_RASTER_TEXT_RUNS = 25_000;
25
+
26
+ export async function renderDocument(
27
+ document: Document,
28
+ options: DocumentRenderOptions = {},
29
+ ): Promise<FileBlob> {
30
+ if (!(document instanceof Document)) throw new Error("Document renderer requires a Document");
31
+ validateRenderOptions(options);
32
+ const format = options.format ?? "png";
33
+ if (format === "html") return new FileBlob([renderHtml(document)], { type: "text/html" });
34
+ if (format === "png") {
35
+ validateRasterBudget(measureCanvas(document), options.scale ?? 1);
36
+ validateRasterComplexity(document);
37
+ }
38
+ const svg = renderSvg(document, options);
39
+ if (format === "svg") return new FileBlob([svg], { type: "image/svg+xml" });
40
+ return FileBlob.fromBytes(await rasterizeSvgToPng(svg), {
41
+ type: "image/png",
42
+ });
43
+ }
44
+
45
+ type StoryVariant = "default" | "first" | "even";
46
+ type ProjectedPage = {
47
+ section: DocumentSection;
48
+ pageNumber: number;
49
+ pageInSection: number;
50
+ blocks: readonly (DocumentParagraph | DocumentTable)[];
51
+ header: { variant: StoryVariant; story: DocumentStory };
52
+ footer: { variant: StoryVariant; story: DocumentStory };
53
+ };
54
+
55
+ function projectPages(document: Document): ProjectedPage[] {
56
+ const pages: ProjectedPage[] = [];
57
+ let pageNumber = 1;
58
+ for (let sectionIndex = 0; sectionIndex < document.sections.items.length; sectionIndex += 1) {
59
+ const section = document.sections.items[sectionIndex]!;
60
+ const end =
61
+ document.sections.items[sectionIndex + 1]?.startBlockIndex ?? document.blocks.items.length;
62
+ const chunks: Array<Array<DocumentParagraph | DocumentTable>> = [[]];
63
+ for (const block of document.blocks.items.slice(section.startBlockIndex, end)) {
64
+ if (block instanceof DocumentPageBreak) chunks.push([]);
65
+ else {
66
+ if (
67
+ block instanceof DocumentParagraph &&
68
+ block.style.pageBreakBefore &&
69
+ chunks[chunks.length - 1]!.length > 0
70
+ )
71
+ chunks.push([]);
72
+ chunks[chunks.length - 1]!.push(block);
73
+ }
74
+ }
75
+ for (let pageInSection = 0; pageInSection < chunks.length; pageInSection += 1) {
76
+ const variant: StoryVariant =
77
+ pageInSection === 0 && section.titlePage
78
+ ? "first"
79
+ : document.evenAndOddHeaders && pageNumber % 2 === 0
80
+ ? "even"
81
+ : "default";
82
+ pages.push({
83
+ section,
84
+ pageNumber,
85
+ pageInSection,
86
+ blocks: chunks[pageInSection]!,
87
+ header: { variant, story: section.headers[variant] },
88
+ footer: { variant, story: section.footers[variant] },
89
+ });
90
+ pageNumber += 1;
91
+ }
92
+ }
93
+ return pages;
94
+ }
95
+
96
+ function renderHtml(document: Document): string {
97
+ const markers = buildListMarkers(document);
98
+ const reviews = indexReviews(document);
99
+ const pages = projectPages(document)
100
+ .map((page) => {
101
+ const body = page.blocks.map((block) => htmlBlock(block, markers, reviews)).join("\n");
102
+ const header = page.header.story.items
103
+ .map((block) => htmlBlock(block, markers, reviews))
104
+ .join("\n");
105
+ const footer = page.footer.story.items
106
+ .map((block) => htmlBlock(block, markers, reviews))
107
+ .join("\n");
108
+ const geometry = page.section.page;
109
+ return `<section data-section-id="${escapeAttribute(page.section.id)}" data-page-number="${page.pageNumber}" data-section-page="${page.pageInSection + 1}" data-header-variant="${page.header.variant}" data-footer-variant="${page.footer.variant}" style="width:${geometry.widthPt}pt;min-height:${geometry.heightPt}pt;padding:${geometry.marginTopPt}pt ${geometry.marginRightPt}pt ${geometry.marginBottomPt}pt ${geometry.marginLeftPt}pt"><header data-story-id="${escapeAttribute(page.header.story.id)}" data-story-variant="${page.header.variant}">${header}</header><main>${body}</main><footer data-story-id="${escapeAttribute(page.footer.story.id)}" data-story-variant="${page.footer.variant}">${footer}</footer></section>`;
110
+ })
111
+ .join("\n");
112
+ return `<!doctype html><html><head><meta charset="utf-8"><style>html{background:#e5e7eb}body{margin:0;color:#111827;font-family:Arial,sans-serif}section{box-sizing:border-box;display:flex;flex-direction:column;margin:16px auto;background:white}main{flex:1}table{border-collapse:collapse;table-layout:fixed}th,td{box-sizing:border-box;vertical-align:middle;text-align:left}p,h1,h2,h3,h4,h5,h6,th,td{white-space:pre-wrap;tab-size:4}p{line-height:1.35}header,footer{color:#4b5563}ins{text-decoration-color:#16a34a;text-decoration-thickness:2px}del{text-decoration-color:#dc2626;text-decoration-thickness:2px}mark[data-comment-ids]{background:#fef3c7;border-bottom:1px solid #d97706}.document-list-marker{display:inline-block;min-width:1.5em;color:#4b5563}</style></head><body>${pages}</body></html>`;
113
+ }
114
+
115
+ function htmlBlock(
116
+ block: DocumentBlock | DocumentStoryBlock,
117
+ markers: ReadonlyMap<string, string>,
118
+ reviews: ReviewIndex,
119
+ ): string {
120
+ if (block instanceof DocumentPageBreak) return "";
121
+ if (block instanceof DocumentTable) {
122
+ const border = color(block.style.borderColor ?? "#D1D5DB");
123
+ const padding = block.style.cellPaddingPt ?? 6;
124
+ const columns = block.style.columnWidthsPt
125
+ ? `<colgroup>${block.style.columnWidthsPt.map((width) => `<col style="width:${width}pt">`).join("")}</colgroup>`
126
+ : "";
127
+ const rows = block.rows
128
+ .map((row, rowIndex) => {
129
+ const header = rowIndex < (block.style.headerRows ?? 0);
130
+ const tag = header ? "th" : "td";
131
+ return `<tr>${row
132
+ .map((cell) => {
133
+ const style = [
134
+ `border:1px solid ${border}`,
135
+ `padding:${padding}pt`,
136
+ header && block.style.headerFill ? `background:${color(block.style.headerFill)}` : "",
137
+ ]
138
+ .filter(Boolean)
139
+ .join(";");
140
+ const content = cell
141
+ .map(
142
+ (run) =>
143
+ `<span style="${escapeAttribute(runCss(run.style))}">${escapeText(run.text)}</span>`,
144
+ )
145
+ .join("");
146
+ return `<${tag} style="${escapeAttribute(style)}">${content}</${tag}>`;
147
+ })
148
+ .join("")}</tr>`;
149
+ })
150
+ .join("");
151
+ return `<table style="${escapeAttribute(tableCss(block.style))}">${columns}${rows}</table>`;
152
+ }
153
+ const tag = block.style.headingLevel ? `h${block.style.headingLevel}` : "p";
154
+ const marker = block.style.list
155
+ ? `<span class="document-list-marker" aria-hidden="true">${escapeText(markers.get(block.id) ?? "")}</span>`
156
+ : "";
157
+ return `<${tag} data-block-id="${escapeAttribute(block.id)}" style="${escapeAttribute(paragraphCss(block.style))}">${marker}${reviewedParagraphHtml(block, reviews)}</${tag}>`;
158
+ }
159
+
160
+ type ReviewIndex = {
161
+ comments: ReadonlyMap<string, readonly DocumentCommentThread[]>;
162
+ changes: ReadonlyMap<string, readonly TrackedChange[]>;
163
+ };
164
+
165
+ function indexReviews(document: Document): ReviewIndex {
166
+ const comments = new Map<string, DocumentCommentThread[]>();
167
+ const changes = new Map<string, TrackedChange[]>();
168
+ for (const item of document.comments.items) append(comments, item.blockId, item);
169
+ for (const item of document.changes.items) append(changes, item.blockId, item);
170
+ return { comments, changes };
171
+ }
172
+
173
+ function reviewedParagraphHtml(block: DocumentParagraph, reviews: ReviewIndex): string {
174
+ const comments = reviews.comments.get(block.id) ?? [];
175
+ const changes = reviews.changes.get(block.id) ?? [];
176
+ const boundaries = new Set<number>([0, block.text.length]);
177
+ const spans: Array<{ start: number; end: number; run: DocumentTextRun }> = [];
178
+ const points = new Map<number, DocumentCommentThread[]>();
179
+ const commentStarts = new Map<number, DocumentCommentThread[]>();
180
+ const commentEnds = new Map<number, DocumentCommentThread[]>();
181
+ const changeStarts = new Map<number, TrackedChange[]>();
182
+ const changeEnds = new Map<number, TrackedChange[]>();
183
+ let offset = 0;
184
+ for (const run of block.runs) {
185
+ const start = offset;
186
+ offset += run.text.length;
187
+ spans.push({ start, end: offset, run });
188
+ boundaries.add(start);
189
+ boundaries.add(offset);
190
+ }
191
+ for (const comment of comments) {
192
+ boundaries.add(comment.start);
193
+ boundaries.add(comment.end);
194
+ if (comment.start === comment.end) append(points, comment.start, comment);
195
+ else {
196
+ append(commentStarts, comment.start, comment);
197
+ append(commentEnds, comment.end, comment);
198
+ }
199
+ }
200
+ for (const change of changes) {
201
+ boundaries.add(change.start);
202
+ boundaries.add(change.end);
203
+ append(changeStarts, change.start, change);
204
+ append(changeEnds, change.end, change);
205
+ }
206
+ const ordered = [...boundaries].sort((left, right) => left - right);
207
+ const html: string[] = [];
208
+ const activeComments = new Set<DocumentCommentThread>();
209
+ const activeChanges = new Set<TrackedChange>();
210
+ let spanIndex = 0;
211
+ for (let index = 0; index < ordered.length; index += 1) {
212
+ const start = ordered[index]!;
213
+ for (const item of commentEnds.get(start) ?? []) activeComments.delete(item);
214
+ for (const item of changeEnds.get(start) ?? []) activeChanges.delete(item);
215
+ for (const item of commentStarts.get(start) ?? []) activeComments.add(item);
216
+ for (const item of changeStarts.get(start) ?? []) activeChanges.add(item);
217
+ for (const item of points.get(start) ?? []) {
218
+ html.push(
219
+ `<span data-comment-anchor="${escapeAttribute(item.id)}" aria-label="Comment anchor"></span>`,
220
+ );
221
+ }
222
+ if (index === ordered.length - 1) break;
223
+ const end = ordered[index + 1]!;
224
+ while (spans[spanIndex] && spans[spanIndex]!.end <= start) spanIndex += 1;
225
+ const span = spans[spanIndex];
226
+ if (!span) throw new Error(`Document paragraph ${block.id} has a discontinuous run map`);
227
+ if (activeChanges.size > 1)
228
+ throw new Error(`Document paragraph ${block.id} has overlapping tracked changes`);
229
+ let segment = `<span style="${escapeAttribute(runCss(span.run.style))}">${escapeText(span.run.text.slice(start - span.start, end - span.start))}</span>`;
230
+ const change = activeChanges.values().next().value as TrackedChange | undefined;
231
+ if (change) {
232
+ const tag = change.kind === "insert" ? "ins" : "del";
233
+ segment = `<${tag} data-change-id="${escapeAttribute(change.id)}">${segment}</${tag}>`;
234
+ }
235
+ if (activeComments.size > 0) {
236
+ const ids = [...activeComments].map((item) => item.id).join(" ");
237
+ segment = `<mark data-comment-ids="${escapeAttribute(ids)}">${segment}</mark>`;
238
+ }
239
+ html.push(segment);
240
+ }
241
+ return html.join("");
242
+ }
243
+
244
+ function renderSvg(document: Document, options: DocumentRenderOptions): string {
245
+ const scale = options.scale ?? 1;
246
+ const background = color(options.background ?? "#FFFFFF");
247
+ const pages = projectPages(document);
248
+ const width = Math.max(...pages.map((page) => page.section.page.widthPt * (4 / 3)));
249
+ const markers = buildListMarkers(document);
250
+ const gap = 24;
251
+ const elements: string[] = [];
252
+ let canvasY = 0;
253
+ for (const page of pages) {
254
+ const geometry = page.section.page;
255
+ const pageWidth = geometry.widthPt * (4 / 3);
256
+ const pageHeight = pageCanvasHeight(page);
257
+ const left = (width - pageWidth) / 2;
258
+ const contentX = left + geometry.marginLeftPt * (4 / 3);
259
+ const usableWidth = pageWidth - (geometry.marginLeftPt + geometry.marginRightPt) * (4 / 3);
260
+ elements.push(
261
+ `<g data-section-id="${escapeAttribute(page.section.id)}" data-page-number="${page.pageNumber}" data-section-page="${page.pageInSection + 1}"><rect x="${left}" y="${canvasY}" width="${pageWidth}" height="${pageHeight}" fill="${escapeAttribute(background)}"/>`,
262
+ );
263
+ const header = svgBlocks(
264
+ page.header.story.items,
265
+ contentX,
266
+ canvasY + 8,
267
+ usableWidth,
268
+ markers,
269
+ "#4B5563",
270
+ );
271
+ elements.push(
272
+ `<g data-story-id="${escapeAttribute(page.header.story.id)}" data-story-variant="${page.header.variant}">${header.elements.join("")}</g>`,
273
+ );
274
+ const body = svgBlocks(
275
+ page.blocks,
276
+ contentX,
277
+ canvasY + geometry.marginTopPt * (4 / 3),
278
+ usableWidth,
279
+ markers,
280
+ "#111827",
281
+ );
282
+ elements.push(`<g data-document-body="true">${body.elements.join("")}</g>`);
283
+ const footerHeight = estimateBlocks(page.footer.story.items);
284
+ const footer = svgBlocks(
285
+ page.footer.story.items,
286
+ contentX,
287
+ canvasY + pageHeight - geometry.marginBottomPt * (4 / 3) - footerHeight,
288
+ usableWidth,
289
+ markers,
290
+ "#4B5563",
291
+ );
292
+ elements.push(
293
+ `<g data-story-id="${escapeAttribute(page.footer.story.id)}" data-story-variant="${page.footer.variant}">${footer.elements.join("")}</g></g>`,
294
+ );
295
+ canvasY += pageHeight + gap;
296
+ }
297
+ const height = Math.max(1, canvasY - gap);
298
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width * scale}" height="${height * scale}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="${escapeAttribute(background)}"/>${elements.join("")}</svg>`;
299
+ }
300
+
301
+ function svgBlocks(
302
+ blocks: readonly (DocumentParagraph | DocumentTable)[],
303
+ x: number,
304
+ initialY: number,
305
+ usableWidth: number,
306
+ markers: ReadonlyMap<string, string>,
307
+ fallbackColor: string,
308
+ ): { elements: string[]; bottom: number } {
309
+ const elements: string[] = [];
310
+ let y = initialY;
311
+ for (const block of blocks) {
312
+ if (block instanceof DocumentTable) {
313
+ const columns = block.rows[0]?.length ?? 1;
314
+ const totalWidth =
315
+ block.style.widthPt !== undefined ? block.style.widthPt * (4 / 3) : usableWidth;
316
+ const widths =
317
+ block.style.columnWidthsPt?.map((value) => value * (4 / 3)) ??
318
+ Array.from({ length: columns }, () => totalWidth / columns);
319
+ const padding = (block.style.cellPaddingPt ?? 6) * (4 / 3);
320
+ block.rows.forEach((row, rowIndex) => {
321
+ const rowHeight = svgTableRowHeight(row, padding);
322
+ const maximumFontSize = row.reduce(
323
+ (maximum, cell) =>
324
+ cell.reduce(
325
+ (cellMaximum, run) => Math.max(cellMaximum, (run.style.fontSizePt ?? 10.5) * (4 / 3)),
326
+ maximum,
327
+ ),
328
+ 14,
329
+ );
330
+ let cellX = x;
331
+ row.forEach((cell, columnIndex) => {
332
+ const cellWidth = widths[columnIndex] ?? totalWidth / columns;
333
+ const header = rowIndex < (block.style.headerRows ?? 0);
334
+ const fill = header && block.style.headerFill ? color(block.style.headerFill) : "#FFFFFF";
335
+ const content = cell
336
+ .map((run) => {
337
+ const decoration = [
338
+ run.style.underline ? "underline" : "",
339
+ run.style.strike ? "line-through" : "",
340
+ ]
341
+ .filter(Boolean)
342
+ .join(" ");
343
+ return `<tspan font-family="${escapeAttribute(run.style.fontFamily ?? "Arial")}" font-size="${(run.style.fontSizePt ?? 10.5) * (4 / 3)}" font-weight="${run.style.bold || header ? 700 : 400}" font-style="${run.style.italic ? "italic" : "normal"}"${decoration ? ` text-decoration="${decoration}"` : ""} fill="${escapeAttribute(color(run.style.color ?? fallbackColor))}">${escapeText(run.text.replaceAll("\t", " ").replaceAll("\n", " "))}</tspan>`;
344
+ })
345
+ .join("");
346
+ const baseline = y + (rowHeight - maximumFontSize) / 2 + maximumFontSize;
347
+ elements.push(
348
+ `<rect x="${cellX}" y="${y}" width="${cellWidth}" height="${rowHeight}" fill="${escapeAttribute(fill)}" stroke="${escapeAttribute(color(block.style.borderColor ?? "#D1D5DB"))}"/><text x="${cellX + padding}" y="${baseline}">${content}</text>`,
349
+ );
350
+ cellX += cellWidth;
351
+ });
352
+ y += rowHeight;
353
+ });
354
+ y += 16;
355
+ continue;
356
+ }
357
+ const baseSize = block.style.headingLevel
358
+ ? Math.max(18, 34 - block.style.headingLevel * 3)
359
+ : 14;
360
+ const lines = svgLines(block, markers.get(block.id));
361
+ const maxSize = block.runs.reduce(
362
+ (maximum, run) => Math.max(maximum, (run.style.fontSizePt ?? baseSize * 0.75) * (4 / 3)),
363
+ baseSize,
364
+ );
365
+ const lineHeight = maxSize * (block.style.lineHeight ?? 1.35);
366
+ const textX =
367
+ block.style.alignment === "center"
368
+ ? x + usableWidth / 2
369
+ : block.style.alignment === "right"
370
+ ? x + usableWidth
371
+ : x;
372
+ const textAnchor =
373
+ block.style.alignment === "center"
374
+ ? "middle"
375
+ : block.style.alignment === "right"
376
+ ? "end"
377
+ : "start";
378
+ y += block.style.spaceBeforePt ?? 4;
379
+ for (const line of lines) {
380
+ y += maxSize;
381
+ const content = line
382
+ .map(({ text, style }) => {
383
+ const size = (style.fontSizePt ?? baseSize * 0.75) * (4 / 3);
384
+ const decoration = [
385
+ style.underline ? "underline" : "",
386
+ style.strike ? "line-through" : "",
387
+ ]
388
+ .filter(Boolean)
389
+ .join(" ");
390
+ return `<tspan font-family="${escapeAttribute(style.fontFamily ?? "Arial")}" font-size="${size}" font-weight="${style.bold || block.style.headingLevel ? 700 : 400}" font-style="${style.italic ? "italic" : "normal"}"${decoration ? ` text-decoration="${decoration}"` : ""} fill="${escapeAttribute(color(style.color ?? fallbackColor))}">${escapeText(text)}</tspan>`;
391
+ })
392
+ .join("");
393
+ elements.push(`<text x="${textX}" y="${y}" text-anchor="${textAnchor}">${content}</text>`);
394
+ y += Math.max(0, lineHeight - maxSize);
395
+ }
396
+ y += block.style.spaceAfterPt ?? 8;
397
+ }
398
+ return { elements, bottom: y };
399
+ }
400
+
401
+ function svgLines(
402
+ block: DocumentParagraph,
403
+ marker: string | undefined,
404
+ ): Array<Array<{ text: string; style: DocumentTextStyle }>> {
405
+ const lines: Array<Array<{ text: string; style: DocumentTextStyle }>> = [[]];
406
+ if (marker) lines[0]!.push({ text: `${marker} `, style: {} });
407
+ for (const run of block.runs) {
408
+ run.text.split("\n").forEach((chunk, index) => {
409
+ if (index > 0) lines.push([]);
410
+ if (chunk.length > 0)
411
+ lines.at(-1)!.push({ text: chunk.replaceAll("\t", " "), style: run.style });
412
+ });
413
+ }
414
+ return lines;
415
+ }
416
+
417
+ function buildListMarkers(document: Document): Map<string, string> {
418
+ const result = new Map<string, string>();
419
+ const visit = (blocks: readonly (DocumentBlock | DocumentStoryBlock)[]): void => {
420
+ let activeKind: "bullet" | "number" | undefined;
421
+ const implicit = Array.from({ length: 9 }, () => 0);
422
+ const explicit = new Map<string, number[]>();
423
+ for (const block of blocks) {
424
+ if (!(block instanceof DocumentParagraph) || !block.style.list) {
425
+ activeKind = undefined;
426
+ implicit.fill(0);
427
+ continue;
428
+ }
429
+ const level = block.style.list.level ?? 0;
430
+ const id = block.style.list.instanceId;
431
+ let counters: number[];
432
+ if (id) {
433
+ const key = `${block.style.list.kind}:${id}`;
434
+ counters = explicit.get(key) ?? Array.from({ length: 9 }, () => 0);
435
+ explicit.set(key, counters);
436
+ activeKind = undefined;
437
+ } else {
438
+ if (activeKind !== block.style.list.kind) implicit.fill(0);
439
+ activeKind = block.style.list.kind;
440
+ counters = implicit;
441
+ }
442
+ if (block.style.list.kind === "number") {
443
+ counters[level] = (counters[level] ?? 0) + 1;
444
+ for (let deeper = level + 1; deeper < counters.length; deeper += 1) counters[deeper] = 0;
445
+ result.set(block.id, `${counters[level]}.`);
446
+ } else result.set(block.id, ["•", "◦", "▪"][level % 3]!);
447
+ }
448
+ };
449
+ visit(document.blocks.items);
450
+ for (const section of document.sections.items)
451
+ for (const story of section.allStories()) visit(story.items);
452
+ return result;
453
+ }
454
+
455
+ function estimateBlocks(blocks: readonly (DocumentParagraph | DocumentTable)[]): number {
456
+ let height = 0;
457
+ for (const block of blocks) {
458
+ if (block instanceof DocumentTable) {
459
+ const padding = (block.style.cellPaddingPt ?? 6) * (4 / 3);
460
+ height += block.rows.reduce((sum, row) => sum + svgTableRowHeight(row, padding), 0) + 16;
461
+ continue;
462
+ }
463
+ const base = block.style.headingLevel ? Math.max(18, 34 - block.style.headingLevel * 3) : 14;
464
+ const size = block.runs.reduce(
465
+ (maximum, run) => Math.max(maximum, (run.style.fontSizePt ?? base * 0.75) * (4 / 3)),
466
+ base,
467
+ );
468
+ height +=
469
+ (block.style.spaceBeforePt ?? 4) +
470
+ size * (block.style.lineHeight ?? 1.35) * Math.max(1, block.text.split("\n").length) +
471
+ (block.style.spaceAfterPt ?? 8);
472
+ }
473
+ return height;
474
+ }
475
+
476
+ function svgTableRowHeight(row: readonly (readonly DocumentTextRun[])[], padding: number): number {
477
+ const maximumFontSize = row.reduce(
478
+ (maximum, cell) =>
479
+ cell.reduce(
480
+ (cellMaximum, run) => Math.max(cellMaximum, (run.style.fontSizePt ?? 10.5) * (4 / 3)),
481
+ maximum,
482
+ ),
483
+ 14,
484
+ );
485
+ return Math.max(32, maximumFontSize * 1.2 + padding * 2);
486
+ }
487
+
488
+ function pageCanvasHeight(page: ProjectedPage): number {
489
+ const physical = page.section.page.heightPt * (4 / 3);
490
+ const content =
491
+ (page.section.page.marginTopPt + page.section.page.marginBottomPt) * (4 / 3) +
492
+ estimateBlocks(page.blocks);
493
+ return Math.max(physical, content);
494
+ }
495
+
496
+ function measureCanvas(document: Document): { width: number; height: number } {
497
+ const pages = projectPages(document);
498
+ return {
499
+ width: Math.max(...pages.map((page) => page.section.page.widthPt * (4 / 3))),
500
+ height: Math.max(1, pages.reduce((sum, page) => sum + pageCanvasHeight(page) + 24, 0) - 24),
501
+ };
502
+ }
503
+
504
+ function validateRasterBudget(dimensions: { width: number; height: number }, scale: number): void {
505
+ const width = Math.ceil(dimensions.width * scale);
506
+ const height = Math.ceil(dimensions.height * scale);
507
+ const dimension = Math.max(width, height);
508
+ if (dimension > MAX_RASTER_DIMENSION) {
509
+ throw new ArtifactLimitError("document raster dimension", dimension, MAX_RASTER_DIMENSION);
510
+ }
511
+ const pixels = width * height;
512
+ if (!Number.isSafeInteger(pixels) || pixels > MAX_RASTER_PIXELS) {
513
+ throw new ArtifactLimitError("document raster pixels", pixels, MAX_RASTER_PIXELS);
514
+ }
515
+ }
516
+
517
+ function validateRasterComplexity(document: Document): void {
518
+ let glyphs = 0;
519
+ let runs = 0;
520
+ for (const block of document.allStoryBlocks()) {
521
+ if (block instanceof DocumentPageBreak) continue;
522
+ if (block instanceof DocumentParagraph) {
523
+ glyphs += block.text.length;
524
+ runs += block.runs.length;
525
+ } else
526
+ for (const row of block.rows)
527
+ for (const cell of row)
528
+ for (const run of cell) {
529
+ glyphs += run.text.length;
530
+ runs += 1;
531
+ }
532
+ if (glyphs > MAX_RASTER_GLYPHS)
533
+ throw new ArtifactLimitError("document raster glyphs", glyphs, MAX_RASTER_GLYPHS);
534
+ if (runs > MAX_RASTER_TEXT_RUNS)
535
+ throw new ArtifactLimitError("document raster text runs", runs, MAX_RASTER_TEXT_RUNS);
536
+ }
537
+ }
538
+
539
+ function paragraphCss(style: DocumentParagraph["style"]): string {
540
+ return [
541
+ style.alignment ? `text-align:${style.alignment}` : "",
542
+ style.spaceBeforePt !== undefined ? `margin-top:${style.spaceBeforePt}pt` : "",
543
+ style.spaceAfterPt !== undefined ? `margin-bottom:${style.spaceAfterPt}pt` : "",
544
+ style.lineHeight !== undefined ? `line-height:${style.lineHeight}` : "",
545
+ style.keepNext ? "break-after:avoid" : "",
546
+ style.pageBreakBefore ? "break-before:page" : "",
547
+ ]
548
+ .filter(Boolean)
549
+ .join(";");
550
+ }
551
+
552
+ function tableCss(style: DocumentTableStyle): string {
553
+ return [
554
+ style.widthPt !== undefined ? `width:${style.widthPt}pt` : "width:100%",
555
+ "table-layout:fixed",
556
+ style.allowRowSplit !== true ? "break-inside:avoid" : "",
557
+ ]
558
+ .filter(Boolean)
559
+ .join(";");
560
+ }
561
+
562
+ function runCss(style: DocumentTextStyle): string {
563
+ const decoration = [style.underline ? "underline" : "", style.strike ? "line-through" : ""]
564
+ .filter(Boolean)
565
+ .join(" ");
566
+ return [
567
+ `font-family:${style.fontFamily ?? "Arial"}`,
568
+ `font-size:${style.fontSizePt ?? 11}pt`,
569
+ `color:${color(style.color ?? "#111827")}`,
570
+ style.bold ? "font-weight:700" : "",
571
+ style.italic ? "font-style:italic" : "",
572
+ decoration ? `text-decoration:${decoration}` : "",
573
+ ]
574
+ .filter(Boolean)
575
+ .join(";");
576
+ }
577
+
578
+ function validateRenderOptions(options: DocumentRenderOptions): void {
579
+ if (typeof options !== "object" || options === null || Array.isArray(options))
580
+ throw new Error("Document render options must be an object");
581
+ for (const key of Object.keys(options)) {
582
+ if (!["format", "scale", "background"].includes(key))
583
+ throw new Error(`Unknown document render options property: ${key}`);
584
+ const descriptor = Object.getOwnPropertyDescriptor(options, key);
585
+ if (!descriptor || !("value" in descriptor))
586
+ throw new Error("Document render options must contain plain data properties");
587
+ }
588
+ if (options.format !== undefined && !["html", "svg", "png"].includes(options.format))
589
+ throw new Error(`Unsupported document render format: ${String(options.format)}`);
590
+ if (
591
+ options.scale !== undefined &&
592
+ (!Number.isFinite(options.scale) || options.scale < 0.1 || options.scale > 8)
593
+ )
594
+ throw new Error("document render scale must be between 0.1 and 8");
595
+ if (options.background !== undefined) color(options.background);
596
+ }
597
+
598
+ function append<K, T>(map: Map<K, T[]>, key: K, value: T): void {
599
+ const values = map.get(key);
600
+ if (values) values.push(value);
601
+ else map.set(key, [value]);
602
+ }
603
+
604
+ function color(value: string): string {
605
+ if (!/^#?[0-9A-Fa-f]{6}$/.test(value)) throw new Error(`Invalid or unsafe color: ${value}`);
606
+ return `#${value.replace(/^#/, "").toUpperCase()}`;
607
+ }
608
+
609
+ function escapeText(value: string): string {
610
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
611
+ }
612
+
613
+ function escapeAttribute(value: string): string {
614
+ return escapeText(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;");
615
+ }