web-doc 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/THIRD_PARTY_NOTICES.md +26 -1
  2. package/dist/adapters/docx-images.d.ts +13 -4
  3. package/dist/adapters/docx-images.js +3 -0
  4. package/dist/adapters/docx-paragraphs.d.ts +65 -0
  5. package/dist/adapters/docx-paragraphs.js +165 -0
  6. package/dist/adapters/docx-prepass.d.ts +17 -0
  7. package/dist/adapters/docx-prepass.js +230 -0
  8. package/dist/adapters/office.d.ts +31 -5
  9. package/dist/adapters/office.js +60 -29
  10. package/dist/adapters/pdf.d.ts +9 -0
  11. package/dist/adapters/pdf.js +10 -0
  12. package/dist/assets/pdfium/pdfium.wasm +0 -0
  13. package/dist/contracts.d.ts +47 -2
  14. package/dist/edit/assets.d.ts +23 -0
  15. package/dist/edit/assets.js +75 -0
  16. package/dist/edit/docx/elements.d.ts +6 -0
  17. package/dist/edit/docx/elements.js +90 -0
  18. package/dist/edit/docx/engine.d.ts +51 -0
  19. package/dist/edit/docx/engine.js +473 -0
  20. package/dist/edit/docx/handlers.d.ts +3 -0
  21. package/dist/edit/docx/handlers.js +15 -0
  22. package/dist/edit/docx/ids.d.ts +45 -0
  23. package/dist/edit/docx/ids.js +101 -0
  24. package/dist/edit/docx/model.d.ts +92 -0
  25. package/dist/edit/docx/model.js +339 -0
  26. package/dist/edit/docx/operations.d.ts +37 -0
  27. package/dist/edit/docx/operations.js +3 -0
  28. package/dist/edit/docx/provider.d.ts +13 -0
  29. package/dist/edit/docx/provider.js +34 -0
  30. package/dist/edit/docx/schemas.d.ts +6 -0
  31. package/dist/edit/docx/schemas.js +192 -0
  32. package/dist/edit/docx/session.d.ts +37 -0
  33. package/dist/edit/docx/session.js +426 -0
  34. package/dist/edit/docx/structure-ops.d.ts +6 -0
  35. package/dist/edit/docx/structure-ops.js +494 -0
  36. package/dist/edit/docx/style.d.ts +45 -0
  37. package/dist/edit/docx/style.js +375 -0
  38. package/dist/edit/docx/table-ops.d.ts +4 -0
  39. package/dist/edit/docx/table-ops.js +219 -0
  40. package/dist/edit/docx/text-ops.d.ts +18 -0
  41. package/dist/edit/docx/text-ops.js +458 -0
  42. package/dist/edit/docx/text.d.ts +34 -0
  43. package/dist/edit/docx/text.js +263 -0
  44. package/dist/edit/docx/types.d.ts +191 -0
  45. package/dist/edit/docx/types.js +1 -0
  46. package/dist/edit/docx/write.d.ts +50 -0
  47. package/dist/edit/docx/write.js +352 -0
  48. package/dist/edit/engine.d.ts +117 -0
  49. package/dist/edit/engine.js +1 -0
  50. package/dist/edit/history.d.ts +53 -0
  51. package/dist/edit/history.js +101 -0
  52. package/dist/edit/ooxml/names.d.ts +20 -0
  53. package/dist/edit/ooxml/names.js +61 -0
  54. package/dist/edit/ooxml/opc.d.ts +50 -0
  55. package/dist/edit/ooxml/opc.js +150 -0
  56. package/dist/edit/ooxml/package.d.ts +82 -0
  57. package/dist/edit/ooxml/package.js +233 -0
  58. package/dist/edit/ooxml/patch.d.ts +51 -0
  59. package/dist/edit/ooxml/patch.js +250 -0
  60. package/dist/edit/ooxml/transaction.d.ts +39 -0
  61. package/dist/edit/ooxml/transaction.js +316 -0
  62. package/dist/edit/ooxml/worker.d.ts +8 -0
  63. package/dist/edit/ooxml/worker.js +12 -0
  64. package/dist/edit/ooxml/writer.d.ts +21 -0
  65. package/dist/edit/ooxml/writer.js +187 -0
  66. package/dist/edit/ooxml/xml.d.ts +74 -0
  67. package/dist/edit/ooxml/xml.js +451 -0
  68. package/dist/edit/ooxml/zip.d.ts +54 -0
  69. package/dist/edit/ooxml/zip.js +280 -0
  70. package/dist/edit/operations.d.ts +19 -0
  71. package/dist/edit/operations.js +137 -0
  72. package/dist/edit/pdf/engine/compact.d.ts +6 -0
  73. package/dist/edit/pdf/engine/compact.js +442 -0
  74. package/dist/edit/pdf/engine/document.d.ts +95 -0
  75. package/dist/edit/pdf/engine/document.js +868 -0
  76. package/dist/edit/pdf/engine/elements.d.ts +45 -0
  77. package/dist/edit/pdf/engine/elements.js +313 -0
  78. package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
  79. package/dist/edit/pdf/engine/existing-text.js +424 -0
  80. package/dist/edit/pdf/engine/fonts.d.ts +78 -0
  81. package/dist/edit/pdf/engine/fonts.js +466 -0
  82. package/dist/edit/pdf/engine/geometry.d.ts +36 -0
  83. package/dist/edit/pdf/engine/geometry.js +97 -0
  84. package/dist/edit/pdf/engine/handler.d.ts +21 -0
  85. package/dist/edit/pdf/engine/handler.js +108 -0
  86. package/dist/edit/pdf/engine/images.d.ts +33 -0
  87. package/dist/edit/pdf/engine/images.js +188 -0
  88. package/dist/edit/pdf/engine/layout.d.ts +23 -0
  89. package/dist/edit/pdf/engine/layout.js +177 -0
  90. package/dist/edit/pdf/engine/operations.d.ts +67 -0
  91. package/dist/edit/pdf/engine/operations.js +3 -0
  92. package/dist/edit/pdf/engine/pages.d.ts +6 -0
  93. package/dist/edit/pdf/engine/pages.js +98 -0
  94. package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
  95. package/dist/edit/pdf/engine/pdfium.js +249 -0
  96. package/dist/edit/pdf/engine/shapes.d.ts +4 -0
  97. package/dist/edit/pdf/engine/shapes.js +183 -0
  98. package/dist/edit/pdf/engine/tables.d.ts +44 -0
  99. package/dist/edit/pdf/engine/tables.js +315 -0
  100. package/dist/edit/pdf/engine/text-box.d.ts +71 -0
  101. package/dist/edit/pdf/engine/text-box.js +317 -0
  102. package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
  103. package/dist/edit/pdf/engine/text-layout.js +67 -0
  104. package/dist/edit/pdf/engine/transform.d.ts +5 -0
  105. package/dist/edit/pdf/engine/transform.js +137 -0
  106. package/dist/edit/pdf/provider.d.ts +35 -0
  107. package/dist/edit/pdf/provider.js +92 -0
  108. package/dist/edit/pdf/range-map.d.ts +14 -0
  109. package/dist/edit/pdf/range-map.js +107 -0
  110. package/dist/edit/pdf/schemas.d.ts +11 -0
  111. package/dist/edit/pdf/schemas.js +291 -0
  112. package/dist/edit/pdf/selection.d.ts +5 -0
  113. package/dist/edit/pdf/selection.js +152 -0
  114. package/dist/edit/pdf/session.d.ts +53 -0
  115. package/dist/edit/pdf/session.js +273 -0
  116. package/dist/edit/pdf/types.d.ts +336 -0
  117. package/dist/edit/pdf/types.js +1 -0
  118. package/dist/edit/pptx/elements.d.ts +74 -0
  119. package/dist/edit/pptx/elements.js +301 -0
  120. package/dist/edit/pptx/engine.d.ts +37 -0
  121. package/dist/edit/pptx/engine.js +466 -0
  122. package/dist/edit/pptx/geometry.d.ts +72 -0
  123. package/dist/edit/pptx/geometry.js +193 -0
  124. package/dist/edit/pptx/handler.d.ts +7 -0
  125. package/dist/edit/pptx/handler.js +91 -0
  126. package/dist/edit/pptx/handlers.d.ts +3 -0
  127. package/dist/edit/pptx/handlers.js +21 -0
  128. package/dist/edit/pptx/image-table-ops.d.ts +5 -0
  129. package/dist/edit/pptx/image-table-ops.js +233 -0
  130. package/dist/edit/pptx/model.d.ts +69 -0
  131. package/dist/edit/pptx/model.js +170 -0
  132. package/dist/edit/pptx/operations.d.ts +50 -0
  133. package/dist/edit/pptx/operations.js +11 -0
  134. package/dist/edit/pptx/provider.d.ts +17 -0
  135. package/dist/edit/pptx/provider.js +40 -0
  136. package/dist/edit/pptx/schemas.d.ts +6 -0
  137. package/dist/edit/pptx/schemas.js +220 -0
  138. package/dist/edit/pptx/session.d.ts +44 -0
  139. package/dist/edit/pptx/session.js +114 -0
  140. package/dist/edit/pptx/shape-ops.d.ts +10 -0
  141. package/dist/edit/pptx/shape-ops.js +486 -0
  142. package/dist/edit/pptx/slide-ops.d.ts +6 -0
  143. package/dist/edit/pptx/slide-ops.js +353 -0
  144. package/dist/edit/pptx/style.d.ts +31 -0
  145. package/dist/edit/pptx/style.js +181 -0
  146. package/dist/edit/pptx/text-ops.d.ts +13 -0
  147. package/dist/edit/pptx/text-ops.js +267 -0
  148. package/dist/edit/pptx/text-write.d.ts +67 -0
  149. package/dist/edit/pptx/text-write.js +293 -0
  150. package/dist/edit/pptx/text.d.ts +41 -0
  151. package/dist/edit/pptx/text.js +99 -0
  152. package/dist/edit/pptx/types.d.ts +232 -0
  153. package/dist/edit/pptx/types.js +1 -0
  154. package/dist/edit/schema.d.ts +11 -0
  155. package/dist/edit/schema.js +275 -0
  156. package/dist/edit/session.d.ts +66 -0
  157. package/dist/edit/session.js +701 -0
  158. package/dist/edit/sessions.d.ts +8 -0
  159. package/dist/edit/sessions.js +1 -0
  160. package/dist/edit/types.d.ts +276 -0
  161. package/dist/edit/types.js +1 -0
  162. package/dist/edit/worker-engine.d.ts +31 -0
  163. package/dist/edit/worker-engine.js +91 -0
  164. package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
  165. package/dist/fonts/manifest.json +6 -1
  166. package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
  167. package/dist/fonts.d.ts +2 -0
  168. package/dist/fonts.js +4 -0
  169. package/dist/headless.d.ts +4 -0
  170. package/dist/headless.js +4 -0
  171. package/dist/index.d.ts +5 -0
  172. package/dist/index.js +5 -0
  173. package/dist/limits.js +3 -0
  174. package/dist/ooxml-edit-worker.d.ts +1 -0
  175. package/dist/ooxml-edit-worker.js +8 -0
  176. package/dist/pdf-edit-worker.d.ts +1 -0
  177. package/dist/pdf-edit-worker.js +35 -0
  178. package/dist/spreadsheet-viewport.d.ts +5 -0
  179. package/dist/spreadsheet-viewport.js +11 -0
  180. package/dist/ui.js +7 -0
  181. package/dist/viewer.d.ts +6 -0
  182. package/dist/viewer.js +293 -19
  183. package/dist/viewport.d.ts +13 -0
  184. package/dist/viewport.js +126 -1
  185. package/dist/worker-protocol.d.ts +39 -2
  186. package/dist/workers/ooxml-edit-worker.js +8671 -0
  187. package/dist/workers/pdf-edit-worker.js +10844 -0
  188. package/package.json +3 -3
@@ -0,0 +1,267 @@
1
+ import { patches } from "../ooxml/patch.js";
2
+ import { committedParts, } from "./operations.js";
3
+ import { alignedParagraphProperties, bodyPrefix, bodySuffix, changedRunProperties, colorProblem, itemsOfSegment, normalizeText, paragraphAt, paragraphXml, renamedProperties, runProperties, sliceOf, styleAt, textProblem, PARAGRAPH_BREAK, } from "./text-write.js";
4
+ /** Resolves a text target; reports why it cannot be edited. */
5
+ async function textTarget(id, context, issue) {
6
+ const record = await context.locate(id);
7
+ if (!record) {
8
+ issue("/target", "unknown-target", `No element ${id}`);
9
+ return undefined;
10
+ }
11
+ if (record.readOnly || !record.txBody || !record.text) {
12
+ issue("/target", "invalid-target", `Element ${id} has no editable text body`);
13
+ return undefined;
14
+ }
15
+ return {
16
+ record,
17
+ part: record.part,
18
+ txBody: record.txBody,
19
+ model: record.text,
20
+ };
21
+ }
22
+ /** Checks a range against its target; returns the clamped offsets. */
23
+ function checkRange(target, id, range, issue) {
24
+ const length = target.model.text.length;
25
+ if (!range)
26
+ return { start: 0, end: length };
27
+ if (range.start.elementId !== id || range.end.elementId !== id) {
28
+ issue("/range", "invalid-range", "Both ends must lie on the target");
29
+ return undefined;
30
+ }
31
+ const { start } = range;
32
+ const { end } = range;
33
+ if (start.offset > length ||
34
+ end.offset > length ||
35
+ start.offset > end.offset) {
36
+ issue("/range", "invalid-range", `Offsets must satisfy 0 ≤ start ≤ end ≤ ${length}`);
37
+ return undefined;
38
+ }
39
+ const text = target.model.text;
40
+ for (const offset of [start.offset, end.offset])
41
+ if (offset > 0 &&
42
+ offset < length &&
43
+ (text.charCodeAt(offset) & 0xfc00) === 0xdc00 &&
44
+ (text.charCodeAt(offset - 1) & 0xfc00) === 0xd800) {
45
+ issue("/range", "invalid-range", "A range cannot split a surrogate pair");
46
+ return undefined;
47
+ }
48
+ for (const paragraph of target.model.paragraphs)
49
+ for (const run of paragraph.runs)
50
+ if (run.kind === "fld" &&
51
+ ((run.start < start.offset && start.offset < run.end) ||
52
+ (run.start < end.offset && end.offset < run.end))) {
53
+ issue("/range", "invalid-range", "A range cannot start or end inside a field");
54
+ return undefined;
55
+ }
56
+ return { start: start.offset, end: end.offset };
57
+ }
58
+ /** Items of a run restricted to `[from, to)` of its text, as bytes when whole. */
59
+ function runItems(part, run, from, to) {
60
+ if (to <= from)
61
+ return [];
62
+ if (from === run.start && to === run.end)
63
+ return [{ kind: "raw", xml: sliceOf(part, run.node) }];
64
+ if (run.kind === "br")
65
+ return [{ kind: "br", rPr: runProperties(part, run) }];
66
+ return [
67
+ {
68
+ kind: "run",
69
+ rPr: runProperties(part, run),
70
+ text: run.text.slice(from - run.start, to - run.start),
71
+ },
72
+ ];
73
+ }
74
+ function draftOf(part, paragraph) {
75
+ return {
76
+ pPr: sliceOf(part, paragraph.pPr),
77
+ items: [],
78
+ endParaRPr: sliceOf(part, paragraph.endParaRPr),
79
+ };
80
+ }
81
+ /** The whole body's new content with the paragraphs `[first, last]` replaced by `drafts`. */
82
+ function bodyContent(part, txBody, model, first, last, drafts) {
83
+ const paragraphs = model.paragraphs;
84
+ const prefix = bodyPrefix(part, txBody, paragraphs[0]?.node);
85
+ const suffix = bodySuffix(part, txBody, paragraphs.at(-1)?.node);
86
+ const before = paragraphs
87
+ .slice(0, first)
88
+ .map((paragraph) => sliceOf(part, paragraph.node))
89
+ .join("");
90
+ const after = paragraphs
91
+ .slice(last + 1)
92
+ .map((paragraph) => sliceOf(part, paragraph.node))
93
+ .join("");
94
+ return `${prefix}${before}${drafts.map(paragraphXml).join("")}${after}${suffix}`;
95
+ }
96
+ function commitBody(context, target, content) {
97
+ const transaction = context.pkg.transaction();
98
+ transaction.patch(target.part, [
99
+ patches.replaceContent(target.part, target.txBody, content),
100
+ ]);
101
+ return transaction.commit().then((change) => ({
102
+ createdIds: [],
103
+ changedPages: [target.record.element.pageIndex],
104
+ warnings: change.warnings,
105
+ parts: committedParts(change),
106
+ }));
107
+ }
108
+ export const replaceTextHandler = {
109
+ async validate(operation, context, issue) {
110
+ const problem = textProblem(normalizeText(operation.text));
111
+ if (problem)
112
+ issue("/text", "invalid-text", `The text holds ${problem}`);
113
+ const target = await textTarget(operation.target, context, issue);
114
+ if (target)
115
+ checkRange(target, operation.target, operation.range, issue);
116
+ },
117
+ async apply(operation, context) {
118
+ const target = (await textTarget(operation.target, context, () => { }));
119
+ const { start, end } = checkRange(target, operation.target, operation.range, () => { });
120
+ return commitBody(context, target, replacedBodyContent(target.part, target.txBody, target.model, normalizeText(operation.text), start, end, !operation.range));
121
+ },
122
+ };
123
+ /**
124
+ * The new content of a text body with `[start, end)` of its text replaced:
125
+ * the paragraphs the range touches are rebuilt, the others keep their
126
+ * bytes. A whole replacement styles the new text like the first run that
127
+ * had text; a ranged one like the run at the start of the range.
128
+ */
129
+ export function replacedBodyContent(part, txBody, model, text, start, end, whole) {
130
+ const paragraphs = model.paragraphs;
131
+ if (paragraphs.length === 0) {
132
+ // A body without paragraphs: the new text becomes its paragraphs.
133
+ const drafts = text.split(PARAGRAPH_BREAK).map((segment) => ({
134
+ pPr: "",
135
+ items: itemsOfSegment(segment, ""),
136
+ endParaRPr: "",
137
+ }));
138
+ return bodyContent(part, txBody, model, 0, -1, drafts);
139
+ }
140
+ const first = paragraphAt(model, start);
141
+ const last = paragraphAt(model, end);
142
+ const firstParagraph = paragraphs[first];
143
+ const lastParagraph = paragraphs[last];
144
+ const style = whole
145
+ ? firstTextStyle(part, model)
146
+ : end > start
147
+ ? (styleOfRunAt(part, firstParagraph, start) ??
148
+ styleAt(part, firstParagraph, start))
149
+ : styleAt(part, firstParagraph, start);
150
+ const head = [];
151
+ for (const run of firstParagraph.runs)
152
+ head.push(...runItems(part, run, run.start, Math.min(run.end, start)));
153
+ const tail = [];
154
+ for (const run of lastParagraph.runs)
155
+ tail.push(...runItems(part, run, Math.max(run.start, end), run.end));
156
+ const segments = text.split(PARAGRAPH_BREAK);
157
+ const drafts = [];
158
+ segments.forEach((segment, index) => {
159
+ const draft = draftOf(part, firstParagraph);
160
+ if (index === 0)
161
+ draft.items.push(...head);
162
+ draft.items.push(...itemsOfSegment(segment, style));
163
+ if (index === segments.length - 1) {
164
+ draft.items.push(...tail);
165
+ draft.endParaRPr = sliceOf(part, lastParagraph.endParaRPr);
166
+ }
167
+ drafts.push(draft);
168
+ });
169
+ return bodyContent(part, txBody, model, first, last, drafts);
170
+ }
171
+ /** The rPr of the run that holds the first replaced character. */
172
+ function styleOfRunAt(part, paragraph, offset) {
173
+ const run = paragraph.runs.find((candidate) => candidate.kind !== "br" &&
174
+ candidate.start <= offset &&
175
+ offset < candidate.end);
176
+ return run ? runProperties(part, run) : undefined;
177
+ }
178
+ function firstTextStyle(part, model) {
179
+ for (const paragraph of model.paragraphs)
180
+ for (const run of paragraph.runs)
181
+ if (run.kind !== "br" && run.text.length > 0)
182
+ return runProperties(part, run);
183
+ const first = model.paragraphs[0];
184
+ return first?.endParaRPr
185
+ ? renamedProperties(part, first.endParaRPr, "a:rPr")
186
+ : "";
187
+ }
188
+ export const setTextStyleHandler = {
189
+ async validate(operation, context, issue) {
190
+ const { style } = operation;
191
+ if (style.color !== undefined) {
192
+ const problem = colorProblem(style.color);
193
+ if (problem)
194
+ issue("/style/color", "invalid-value", `Colour: ${problem}`);
195
+ }
196
+ if (style.fontFamily !== undefined) {
197
+ const problem = textProblem(style.fontFamily);
198
+ if (problem)
199
+ issue("/style/fontFamily", "invalid-value", `Font: ${problem}`);
200
+ }
201
+ const target = await textTarget(operation.target, context, issue);
202
+ if (target)
203
+ checkRange(target, operation.target, operation.range, issue);
204
+ },
205
+ async apply(operation, context) {
206
+ const target = (await textTarget(operation.target, context, () => { }));
207
+ const { start, end } = checkRange(target, operation.target, operation.range, () => { });
208
+ const { part, model } = target;
209
+ const { style } = operation;
210
+ const { align, ...runChange } = style;
211
+ const changesRuns = Object.keys(runChange).length > 0;
212
+ // Nothing to write: the body keeps its bytes and its autofit scale.
213
+ if (!changesRuns && align === undefined)
214
+ return { createdIds: [], changedPages: [], warnings: [] };
215
+ const paragraphs = model.paragraphs;
216
+ if (paragraphs.length === 0)
217
+ return { createdIds: [], changedPages: [], warnings: [] };
218
+ const first = paragraphAt(model, start);
219
+ const last = paragraphAt(model, end);
220
+ const drafts = [];
221
+ for (let index = first; index <= last; index += 1) {
222
+ const paragraph = paragraphs[index];
223
+ const draft = draftOf(part, paragraph);
224
+ if (align !== undefined)
225
+ draft.pPr = alignedParagraphProperties(part, paragraph.pPr, align);
226
+ for (const run of paragraph.runs) {
227
+ const from = Math.max(run.start, start);
228
+ const to = Math.min(run.end, end);
229
+ if (!changesRuns || to <= from) {
230
+ draft.items.push({ kind: "raw", xml: sliceOf(part, run.node) });
231
+ continue;
232
+ }
233
+ draft.items.push(...runItems(part, run, run.start, from));
234
+ draft.items.push(restyled(part, run, from, to, runChange));
235
+ draft.items.push(...runItems(part, run, to, run.end));
236
+ }
237
+ if (changesRuns && end >= paragraph.end && paragraph.endParaRPr)
238
+ draft.endParaRPr = changedRunProperties(part, paragraph.endParaRPr, runChange, "a:endParaRPr");
239
+ drafts.push(draft);
240
+ }
241
+ return commitBody(context, target, bodyContent(part, target.txBody, model, first, last, drafts));
242
+ },
243
+ };
244
+ /** The covered part of a run with the change applied; a field keeps its own bytes around a new rPr. */
245
+ function restyled(part, run, from, to, change) {
246
+ const rPr = changedRunProperties(part, run.rPr, change);
247
+ if (run.kind === "br")
248
+ return { kind: "br", rPr };
249
+ if (run.kind === "fld") {
250
+ const xml = sliceOf(part, run.node);
251
+ const offset = run.node.start;
252
+ if (run.rPr)
253
+ return {
254
+ kind: "raw",
255
+ xml: xml.slice(0, run.rPr.start - offset) +
256
+ rPr +
257
+ xml.slice(run.rPr.end - offset),
258
+ };
259
+ const at = run.node.contentStart - offset;
260
+ return { kind: "raw", xml: `${xml.slice(0, at)}${rPr}${xml.slice(at)}` };
261
+ }
262
+ return {
263
+ kind: "run",
264
+ rPr,
265
+ text: run.text.slice(from - run.start, to - run.start),
266
+ };
267
+ }
@@ -0,0 +1,67 @@
1
+ import type { EditColor } from "../types.js";
2
+ import { type XmlElement, type XmlPart } from "../ooxml/xml.js";
3
+ import type { ParagraphInfo, RunInfo, TextModel } from "./text.js";
4
+ import { PARAGRAPH_BREAK } from "./text.js";
5
+ import type { PptxTextAlign, PptxTextStyleChange } from "./types.js";
6
+ /** A run of a rebuilt paragraph: untouched bytes, or a run or break with properties. */
7
+ export type RunItem = {
8
+ readonly kind: "raw";
9
+ readonly xml: string;
10
+ } | {
11
+ readonly kind: "run";
12
+ readonly rPr: string;
13
+ readonly text: string;
14
+ } | {
15
+ readonly kind: "br";
16
+ readonly rPr: string;
17
+ };
18
+ export interface ParagraphDraft {
19
+ /** The `a:pPr` bytes, or "". */
20
+ pPr: string;
21
+ items: RunItem[];
22
+ /** The `a:endParaRPr` bytes, or "". */
23
+ endParaRPr: string;
24
+ }
25
+ export declare const THEME_COLORS: Set<string>;
26
+ export declare const COLOR_MODIFIERS: Set<string>;
27
+ /** Why a text cannot be written, or undefined when it can. */
28
+ export declare function textProblem(text: string): string | undefined;
29
+ export declare function colorProblem(color: unknown): string | undefined;
30
+ /** `<a:solidFill>` for a colour in its theme or RGB form. */
31
+ export declare function solidFillXml(color: EditColor): string;
32
+ export declare function alignValue(align: PptxTextAlign): string;
33
+ /** The bytes of a node, or "" without one. */
34
+ export declare function sliceOf(part: XmlPart, node: XmlElement | undefined): string;
35
+ /**
36
+ * Run properties with a change applied: attributes kept and set, children
37
+ * kept in order with the fill and the Latin face replaced where the schema
38
+ * puts them. `source` may be an `a:rPr`, an `a:endParaRPr` (renamed) or
39
+ * nothing.
40
+ */
41
+ export declare function changedRunProperties(part: XmlPart, source: XmlElement | undefined, change: PptxTextStyleChange, tag?: string): string;
42
+ /** Text as the engine stores it: Windows and classic Mac line ends become "\n". */
43
+ export declare function normalizeText(text: string): string;
44
+ /** `a:pPr` bytes with `algn` set, created when the paragraph has none. */
45
+ export declare function alignedParagraphProperties(part: XmlPart, pPr: XmlElement | undefined, align: PptxTextAlign): string;
46
+ /** A run or break as the file will carry it. */
47
+ export declare function runXml(item: RunItem): string;
48
+ export declare function paragraphXml(draft: ParagraphDraft): string;
49
+ /** Items of text with its paragraph breaks removed: runs and breaks in `rPr`. */
50
+ export declare function itemsOfSegment(segment: string, rPr: string): RunItem[];
51
+ /** The rPr a run of inserted text takes at `offset`: the run before it, else the run after, else the paragraph end. */
52
+ export declare function styleAt(part: XmlPart, paragraph: ParagraphInfo, offset: number): string;
53
+ /** The `a:rPr` bytes of a run, or "". */
54
+ export declare function runProperties(part: XmlPart, run: RunInfo): string;
55
+ /** The bytes of a properties element under another tag name. */
56
+ export declare function renamedProperties(part: XmlPart, node: XmlElement, tag: string): string;
57
+ /**
58
+ * The bytes of a text body's content before its first paragraph, with the
59
+ * stale autofit scale dropped: `a:normAutofit`'s `fontScale` and
60
+ * `lnSpcReduction` describe the old text.
61
+ */
62
+ export declare function bodyPrefix(part: XmlPart, txBody: XmlElement, firstParagraph: XmlElement | undefined): string;
63
+ /** The bytes after the last paragraph (an extension list, usually nothing). */
64
+ export declare function bodySuffix(part: XmlPart, txBody: XmlElement, lastParagraph: XmlElement | undefined): string;
65
+ /** The index of the paragraph that holds an offset, taking the one before a break first. */
66
+ export declare function paragraphAt(model: TextModel, offset: number): number;
67
+ export { PARAGRAPH_BREAK };
@@ -0,0 +1,293 @@
1
+ import { escapeText } from "../ooxml/xml.js";
2
+ import { LINE_BREAK, PARAGRAPH_BREAK } from "./text.js";
3
+ const FILLS = new Set([
4
+ "noFill",
5
+ "solidFill",
6
+ "gradFill",
7
+ "blipFill",
8
+ "pattFill",
9
+ "grpFill",
10
+ ]);
11
+ /** Children of a:rPr that follow the fill in the schema. */
12
+ const AFTER_FILL = new Set([
13
+ "effectLst",
14
+ "effectDag",
15
+ "highlight",
16
+ "uLnTx",
17
+ "uLn",
18
+ "uFillTx",
19
+ "uFill",
20
+ "latin",
21
+ "ea",
22
+ "cs",
23
+ "sym",
24
+ "hlinkClick",
25
+ "hlinkMouseOver",
26
+ "rtl",
27
+ "extLst",
28
+ ]);
29
+ /** Children of a:rPr that follow a:latin in the schema. */
30
+ const AFTER_LATIN = new Set([
31
+ "ea",
32
+ "cs",
33
+ "sym",
34
+ "hlinkClick",
35
+ "hlinkMouseOver",
36
+ "rtl",
37
+ "extLst",
38
+ ]);
39
+ export const THEME_COLORS = new Set([
40
+ "bg1",
41
+ "tx1",
42
+ "bg2",
43
+ "tx2",
44
+ "dk1",
45
+ "lt1",
46
+ "dk2",
47
+ "lt2",
48
+ "accent1",
49
+ "accent2",
50
+ "accent3",
51
+ "accent4",
52
+ "accent5",
53
+ "accent6",
54
+ "hlink",
55
+ "folHlink",
56
+ ]);
57
+ export const COLOR_MODIFIERS = new Set([
58
+ "tint",
59
+ "shade",
60
+ "alpha",
61
+ "lumMod",
62
+ "lumOff",
63
+ "satMod",
64
+ "satOff",
65
+ "hueMod",
66
+ "hueOff",
67
+ ]);
68
+ /** Characters XML 1.0 cannot carry; a lone surrogate is caught separately. */
69
+ const FORBIDDEN = /[\u0000-\u0008\u000c\u000e-\u001f\ufffe\uffff]/;
70
+ const LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
71
+ /** Why a text cannot be written, or undefined when it can. */
72
+ export function textProblem(text) {
73
+ if (FORBIDDEN.test(text))
74
+ return "control characters XML cannot carry";
75
+ if (LONE_SURROGATE.test(text))
76
+ return "a lone surrogate";
77
+ return undefined;
78
+ }
79
+ export function colorProblem(color) {
80
+ if (typeof color === "string")
81
+ return /^#[0-9A-Fa-f]{6}$/.test(color) ? undefined : "not #RRGGBB";
82
+ if (!color || typeof color !== "object")
83
+ return "not a colour";
84
+ const { theme, mods } = color;
85
+ if (typeof theme !== "string" || !THEME_COLORS.has(theme))
86
+ return "not a theme colour slot";
87
+ if (mods !== undefined) {
88
+ if (!mods || typeof mods !== "object" || Array.isArray(mods))
89
+ return "mods is not an object";
90
+ for (const [name, value] of Object.entries(mods)) {
91
+ if (!COLOR_MODIFIERS.has(name))
92
+ return `unknown modifier ${name}`;
93
+ if (typeof value !== "number" ||
94
+ !Number.isInteger(value) ||
95
+ Math.abs(value) > 100_000)
96
+ return `modifier ${name} is not an integer within ±100000`;
97
+ }
98
+ }
99
+ return undefined;
100
+ }
101
+ /** `<a:solidFill>` for a colour in its theme or RGB form. */
102
+ export function solidFillXml(color) {
103
+ if (typeof color === "string")
104
+ return `<a:solidFill><a:srgbClr val="${color.slice(1).toUpperCase()}"/></a:solidFill>`;
105
+ const mods = Object.entries(color.mods ?? {})
106
+ .map(([name, value]) => `<a:${name} val="${value}"/>`)
107
+ .join("");
108
+ return mods
109
+ ? `<a:solidFill><a:schemeClr val="${color.theme}">${mods}</a:schemeClr></a:solidFill>`
110
+ : `<a:solidFill><a:schemeClr val="${color.theme}"/></a:solidFill>`;
111
+ }
112
+ export function alignValue(align) {
113
+ switch (align) {
114
+ case "left":
115
+ return "l";
116
+ case "center":
117
+ return "ctr";
118
+ case "right":
119
+ return "r";
120
+ default:
121
+ return "just";
122
+ }
123
+ }
124
+ /** The bytes of a node, or "" without one. */
125
+ export function sliceOf(part, node) {
126
+ return node ? part.text.slice(node.start, node.end) : "";
127
+ }
128
+ /**
129
+ * Run properties with a change applied: attributes kept and set, children
130
+ * kept in order with the fill and the Latin face replaced where the schema
131
+ * puts them. `source` may be an `a:rPr`, an `a:endParaRPr` (renamed) or
132
+ * nothing.
133
+ */
134
+ export function changedRunProperties(part, source, change, tag = "a:rPr") {
135
+ const attributes = new Map();
136
+ const children = [];
137
+ if (source) {
138
+ for (const attribute of source.attributes)
139
+ attributes.set(attribute.name, attribute.rawValue);
140
+ for (const child of source.children)
141
+ children.push({ local: child.local, xml: sliceOf(part, child) });
142
+ }
143
+ if (change.bold !== undefined)
144
+ attributes.set("b", change.bold ? "1" : "0");
145
+ if (change.italic !== undefined)
146
+ attributes.set("i", change.italic ? "1" : "0");
147
+ if (change.underline !== undefined)
148
+ attributes.set("u", change.underline ? "sng" : "none");
149
+ if (change.fontSize !== undefined)
150
+ attributes.set("sz", String(Math.round(change.fontSize * 100)));
151
+ if (change.color !== undefined)
152
+ replaceChild(children, FILLS, AFTER_FILL, "solidFill", solidFillXml(change.color));
153
+ if (change.fontFamily !== undefined)
154
+ replaceChild(children, new Set(["latin"]), AFTER_LATIN, "latin", `<a:latin typeface="${escapeAttributeValue(change.fontFamily)}"/>`);
155
+ const attributeText = [...attributes]
156
+ .map(([name, raw]) => ` ${name}="${raw}"`)
157
+ .join("");
158
+ const content = children.map((child) => child.xml).join("");
159
+ return content
160
+ ? `<${tag}${attributeText}>${content}</${tag}>`
161
+ : `<${tag}${attributeText}/>`;
162
+ }
163
+ function replaceChild(children, replaced, after, local, xml) {
164
+ const first = children.findIndex((child) => replaced.has(child.local));
165
+ for (let index = children.length - 1; index >= 0; index -= 1)
166
+ if (replaced.has(children[index].local))
167
+ children.splice(index, 1);
168
+ let at = first >= 0 ? first : children.findIndex((child) => after.has(child.local));
169
+ if (at < 0)
170
+ at = children.length;
171
+ children.splice(at, 0, { local, xml });
172
+ }
173
+ function escapeAttributeValue(value) {
174
+ return value
175
+ .replaceAll("&", "&amp;")
176
+ .replaceAll("<", "&lt;")
177
+ .replaceAll('"', "&quot;")
178
+ .replaceAll("\t", "&#9;")
179
+ .replaceAll("\n", "&#10;")
180
+ .replaceAll("\r", "&#13;");
181
+ }
182
+ /** Text as the engine stores it: Windows and classic Mac line ends become "\n". */
183
+ export function normalizeText(text) {
184
+ return text.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
185
+ }
186
+ /** `a:pPr` bytes with `algn` set, created when the paragraph has none. */
187
+ export function alignedParagraphProperties(part, pPr, align) {
188
+ const value = alignValue(align);
189
+ if (!pPr)
190
+ return `<a:pPr algn="${value}"/>`;
191
+ const xml = sliceOf(part, pPr);
192
+ const existing = pPr.attributes.find((attribute) => attribute.name === "algn");
193
+ if (existing)
194
+ return (xml.slice(0, existing.start - pPr.start) +
195
+ `algn="${value}"` +
196
+ xml.slice(existing.end - pPr.start));
197
+ const insertAt = pPr.start + 1 + pPr.name.length - pPr.start;
198
+ return `${xml.slice(0, insertAt)} algn="${value}"${xml.slice(insertAt)}`;
199
+ }
200
+ /** A run or break as the file will carry it. */
201
+ export function runXml(item) {
202
+ switch (item.kind) {
203
+ case "raw":
204
+ return item.xml;
205
+ case "br":
206
+ return item.rPr ? `<a:br>${item.rPr}</a:br>` : "<a:br/>";
207
+ default:
208
+ return item.text.length === 0
209
+ ? ""
210
+ : `<a:r>${item.rPr}<a:t>${escapeText(item.text)}</a:t></a:r>`;
211
+ }
212
+ }
213
+ export function paragraphXml(draft) {
214
+ return `<a:p>${draft.pPr}${draft.items.map(runXml).join("")}${draft.endParaRPr}</a:p>`;
215
+ }
216
+ /** Items of text with its paragraph breaks removed: runs and breaks in `rPr`. */
217
+ export function itemsOfSegment(segment, rPr) {
218
+ const items = [];
219
+ const lines = segment.split(LINE_BREAK);
220
+ lines.forEach((line, index) => {
221
+ if (index > 0)
222
+ items.push({ kind: "br", rPr });
223
+ if (line.length > 0)
224
+ items.push({ kind: "run", rPr, text: line });
225
+ });
226
+ return items;
227
+ }
228
+ /** The rPr a run of inserted text takes at `offset`: the run before it, else the run after, else the paragraph end. */
229
+ export function styleAt(part, paragraph, offset) {
230
+ let before;
231
+ for (const run of paragraph.runs)
232
+ if (run.kind !== "br" && run.start < offset && offset <= run.end)
233
+ before = run;
234
+ const after = paragraph.runs.find((run) => run.kind !== "br" && run.start <= offset && offset < run.end);
235
+ const inside = paragraph.runs.find((run) => run.kind !== "br" && run.start < offset && offset < run.end);
236
+ const source = inside ?? before ?? after;
237
+ if (source)
238
+ return runProperties(part, source);
239
+ return paragraph.endParaRPr
240
+ ? renamedProperties(part, paragraph.endParaRPr, "a:rPr")
241
+ : "";
242
+ }
243
+ /** The `a:rPr` bytes of a run, or "". */
244
+ export function runProperties(part, run) {
245
+ return sliceOf(part, run.rPr);
246
+ }
247
+ /** The bytes of a properties element under another tag name. */
248
+ export function renamedProperties(part, node, tag) {
249
+ const xml = sliceOf(part, node);
250
+ if (node.selfClosing)
251
+ return `<${tag}${xml.slice(1 + node.name.length, -2)}/>`;
252
+ return `<${tag}${xml.slice(1 + node.name.length, node.contentStart - node.start)}${xml.slice(node.contentStart - node.start, node.contentEnd - node.start)}</${tag}>`;
253
+ }
254
+ /**
255
+ * The bytes of a text body's content before its first paragraph, with the
256
+ * stale autofit scale dropped: `a:normAutofit`'s `fontScale` and
257
+ * `lnSpcReduction` describe the old text.
258
+ */
259
+ export function bodyPrefix(part, txBody, firstParagraph) {
260
+ const end = firstParagraph ? firstParagraph.start : txBody.contentEnd;
261
+ let prefix = part.text.slice(txBody.contentStart, end);
262
+ const bodyPr = txBody.children.find((child) => child.local === "bodyPr");
263
+ const autofit = bodyPr?.children.find((child) => child.local === "normAutofit");
264
+ if (!autofit)
265
+ return prefix;
266
+ const cuts = autofit.attributes
267
+ .filter((attribute) => ["fontScale", "lnSpcReduction"].includes(attribute.name))
268
+ .map((attribute) => {
269
+ let start = attribute.start;
270
+ while (start > autofit.start && /\s/.test(part.text[start - 1]))
271
+ start -= 1;
272
+ return {
273
+ start: start - txBody.contentStart,
274
+ end: attribute.end - txBody.contentStart,
275
+ };
276
+ })
277
+ .sort((a, b) => b.start - a.start);
278
+ for (const cut of cuts)
279
+ prefix = prefix.slice(0, cut.start) + prefix.slice(cut.end);
280
+ return prefix;
281
+ }
282
+ /** The bytes after the last paragraph (an extension list, usually nothing). */
283
+ export function bodySuffix(part, txBody, lastParagraph) {
284
+ return lastParagraph
285
+ ? part.text.slice(lastParagraph.end, txBody.contentEnd)
286
+ : "";
287
+ }
288
+ /** The index of the paragraph that holds an offset, taking the one before a break first. */
289
+ export function paragraphAt(model, offset) {
290
+ const index = model.paragraphs.findIndex((paragraph) => paragraph.start <= offset && offset <= paragraph.end);
291
+ return index < 0 ? Math.max(0, model.paragraphs.length - 1) : index;
292
+ }
293
+ export { PARAGRAPH_BREAK };
@@ -0,0 +1,41 @@
1
+ import type { XmlElement, XmlPart } from "../ooxml/xml.js";
2
+ export declare const PARAGRAPH_BREAK = "\n";
3
+ export declare const LINE_BREAK = "\v";
4
+ export type RunKind = "r" | "br" | "fld";
5
+ export interface RunInfo {
6
+ readonly node: XmlElement;
7
+ readonly kind: RunKind;
8
+ /** Half-open offsets in the body's text. */
9
+ readonly start: number;
10
+ readonly end: number;
11
+ readonly text: string;
12
+ /** `a:rPr` of the run or field, when present. */
13
+ readonly rPr?: XmlElement;
14
+ }
15
+ export interface ParagraphInfo {
16
+ readonly node: XmlElement;
17
+ /** Half-open offsets in the body's text, the trailing "\n" excluded. */
18
+ readonly start: number;
19
+ readonly end: number;
20
+ readonly runs: readonly RunInfo[];
21
+ readonly pPr?: XmlElement;
22
+ /** 0-based outline level (`a:pPr/@lvl`). */
23
+ readonly level: number;
24
+ readonly endParaRPr?: XmlElement;
25
+ }
26
+ export interface TextModel {
27
+ readonly text: string;
28
+ readonly paragraphs: readonly ParagraphInfo[];
29
+ }
30
+ /** Reads the text of a `p:txBody` or `a:txBody`. */
31
+ export declare function readTextModel(part: XmlPart, txBody: XmlElement): TextModel;
32
+ /** The first run that draws text, with its paragraph; undefined for an empty body. */
33
+ export declare function firstTextRun(model: TextModel): {
34
+ readonly run: RunInfo;
35
+ readonly paragraph: ParagraphInfo;
36
+ } | undefined;
37
+ /** The cells of an `a:tbl`, row by row, as text models. */
38
+ export declare function readTableCells(part: XmlPart, tbl: XmlElement): readonly (readonly {
39
+ node: XmlElement;
40
+ model: TextModel;
41
+ }[])[];