web-doc 0.6.2 → 0.8.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 (201) hide show
  1. package/THIRD_PARTY_NOTICES.md +33 -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 +53 -2
  14. package/dist/edit/ai/outline.d.ts +47 -0
  15. package/dist/edit/ai/outline.js +338 -0
  16. package/dist/edit/ai/targets.d.ts +16 -0
  17. package/dist/edit/ai/targets.js +309 -0
  18. package/dist/edit/ai/tools.d.ts +28 -0
  19. package/dist/edit/ai/tools.js +605 -0
  20. package/dist/edit/ai/types.d.ts +175 -0
  21. package/dist/edit/ai/types.js +1 -0
  22. package/dist/edit/assets.d.ts +23 -0
  23. package/dist/edit/assets.js +75 -0
  24. package/dist/edit/docx/elements.d.ts +6 -0
  25. package/dist/edit/docx/elements.js +90 -0
  26. package/dist/edit/docx/engine.d.ts +57 -0
  27. package/dist/edit/docx/engine.js +547 -0
  28. package/dist/edit/docx/handlers.d.ts +3 -0
  29. package/dist/edit/docx/handlers.js +15 -0
  30. package/dist/edit/docx/ids.d.ts +45 -0
  31. package/dist/edit/docx/ids.js +101 -0
  32. package/dist/edit/docx/model.d.ts +94 -0
  33. package/dist/edit/docx/model.js +350 -0
  34. package/dist/edit/docx/operations.d.ts +47 -0
  35. package/dist/edit/docx/operations.js +3 -0
  36. package/dist/edit/docx/provider.d.ts +16 -0
  37. package/dist/edit/docx/provider.js +37 -0
  38. package/dist/edit/docx/schemas.d.ts +6 -0
  39. package/dist/edit/docx/schemas.js +192 -0
  40. package/dist/edit/docx/session.d.ts +48 -0
  41. package/dist/edit/docx/session.js +467 -0
  42. package/dist/edit/docx/structure-ops.d.ts +6 -0
  43. package/dist/edit/docx/structure-ops.js +529 -0
  44. package/dist/edit/docx/style.d.ts +45 -0
  45. package/dist/edit/docx/style.js +375 -0
  46. package/dist/edit/docx/table-ops.d.ts +4 -0
  47. package/dist/edit/docx/table-ops.js +241 -0
  48. package/dist/edit/docx/text-ops.d.ts +40 -0
  49. package/dist/edit/docx/text-ops.js +491 -0
  50. package/dist/edit/docx/text.d.ts +34 -0
  51. package/dist/edit/docx/text.js +284 -0
  52. package/dist/edit/docx/tracked.d.ts +52 -0
  53. package/dist/edit/docx/tracked.js +347 -0
  54. package/dist/edit/docx/types.d.ts +206 -0
  55. package/dist/edit/docx/types.js +1 -0
  56. package/dist/edit/docx/write.d.ts +64 -0
  57. package/dist/edit/docx/write.js +375 -0
  58. package/dist/edit/engine.d.ts +128 -0
  59. package/dist/edit/engine.js +1 -0
  60. package/dist/edit/history.d.ts +77 -0
  61. package/dist/edit/history.js +123 -0
  62. package/dist/edit/ooxml/names.d.ts +20 -0
  63. package/dist/edit/ooxml/names.js +61 -0
  64. package/dist/edit/ooxml/opc.d.ts +50 -0
  65. package/dist/edit/ooxml/opc.js +150 -0
  66. package/dist/edit/ooxml/package.d.ts +82 -0
  67. package/dist/edit/ooxml/package.js +233 -0
  68. package/dist/edit/ooxml/patch.d.ts +51 -0
  69. package/dist/edit/ooxml/patch.js +250 -0
  70. package/dist/edit/ooxml/transaction.d.ts +39 -0
  71. package/dist/edit/ooxml/transaction.js +316 -0
  72. package/dist/edit/ooxml/worker.d.ts +8 -0
  73. package/dist/edit/ooxml/worker.js +12 -0
  74. package/dist/edit/ooxml/writer.d.ts +21 -0
  75. package/dist/edit/ooxml/writer.js +187 -0
  76. package/dist/edit/ooxml/xml.d.ts +74 -0
  77. package/dist/edit/ooxml/xml.js +451 -0
  78. package/dist/edit/ooxml/zip.d.ts +54 -0
  79. package/dist/edit/ooxml/zip.js +280 -0
  80. package/dist/edit/operations.d.ts +19 -0
  81. package/dist/edit/operations.js +137 -0
  82. package/dist/edit/pdf/engine/compact.d.ts +6 -0
  83. package/dist/edit/pdf/engine/compact.js +442 -0
  84. package/dist/edit/pdf/engine/document.d.ts +95 -0
  85. package/dist/edit/pdf/engine/document.js +868 -0
  86. package/dist/edit/pdf/engine/elements.d.ts +45 -0
  87. package/dist/edit/pdf/engine/elements.js +313 -0
  88. package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
  89. package/dist/edit/pdf/engine/existing-text.js +424 -0
  90. package/dist/edit/pdf/engine/fonts.d.ts +78 -0
  91. package/dist/edit/pdf/engine/fonts.js +466 -0
  92. package/dist/edit/pdf/engine/geometry.d.ts +36 -0
  93. package/dist/edit/pdf/engine/geometry.js +97 -0
  94. package/dist/edit/pdf/engine/handler.d.ts +21 -0
  95. package/dist/edit/pdf/engine/handler.js +108 -0
  96. package/dist/edit/pdf/engine/images.d.ts +33 -0
  97. package/dist/edit/pdf/engine/images.js +188 -0
  98. package/dist/edit/pdf/engine/layout.d.ts +23 -0
  99. package/dist/edit/pdf/engine/layout.js +177 -0
  100. package/dist/edit/pdf/engine/operations.d.ts +67 -0
  101. package/dist/edit/pdf/engine/operations.js +3 -0
  102. package/dist/edit/pdf/engine/pages.d.ts +6 -0
  103. package/dist/edit/pdf/engine/pages.js +98 -0
  104. package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
  105. package/dist/edit/pdf/engine/pdfium.js +249 -0
  106. package/dist/edit/pdf/engine/shapes.d.ts +4 -0
  107. package/dist/edit/pdf/engine/shapes.js +183 -0
  108. package/dist/edit/pdf/engine/tables.d.ts +44 -0
  109. package/dist/edit/pdf/engine/tables.js +315 -0
  110. package/dist/edit/pdf/engine/text-box.d.ts +71 -0
  111. package/dist/edit/pdf/engine/text-box.js +317 -0
  112. package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
  113. package/dist/edit/pdf/engine/text-layout.js +67 -0
  114. package/dist/edit/pdf/engine/transform.d.ts +5 -0
  115. package/dist/edit/pdf/engine/transform.js +137 -0
  116. package/dist/edit/pdf/provider.d.ts +35 -0
  117. package/dist/edit/pdf/provider.js +92 -0
  118. package/dist/edit/pdf/range-map.d.ts +16 -0
  119. package/dist/edit/pdf/range-map.js +107 -0
  120. package/dist/edit/pdf/schemas.d.ts +11 -0
  121. package/dist/edit/pdf/schemas.js +291 -0
  122. package/dist/edit/pdf/selection.d.ts +5 -0
  123. package/dist/edit/pdf/selection.js +152 -0
  124. package/dist/edit/pdf/session.d.ts +63 -0
  125. package/dist/edit/pdf/session.js +312 -0
  126. package/dist/edit/pdf/types.d.ts +336 -0
  127. package/dist/edit/pdf/types.js +1 -0
  128. package/dist/edit/pptx/elements.d.ts +74 -0
  129. package/dist/edit/pptx/elements.js +301 -0
  130. package/dist/edit/pptx/engine.d.ts +37 -0
  131. package/dist/edit/pptx/engine.js +466 -0
  132. package/dist/edit/pptx/geometry.d.ts +72 -0
  133. package/dist/edit/pptx/geometry.js +193 -0
  134. package/dist/edit/pptx/handler.d.ts +7 -0
  135. package/dist/edit/pptx/handler.js +99 -0
  136. package/dist/edit/pptx/handlers.d.ts +3 -0
  137. package/dist/edit/pptx/handlers.js +21 -0
  138. package/dist/edit/pptx/image-table-ops.d.ts +5 -0
  139. package/dist/edit/pptx/image-table-ops.js +233 -0
  140. package/dist/edit/pptx/model.d.ts +69 -0
  141. package/dist/edit/pptx/model.js +170 -0
  142. package/dist/edit/pptx/operations.d.ts +50 -0
  143. package/dist/edit/pptx/operations.js +11 -0
  144. package/dist/edit/pptx/provider.d.ts +17 -0
  145. package/dist/edit/pptx/provider.js +40 -0
  146. package/dist/edit/pptx/schemas.d.ts +6 -0
  147. package/dist/edit/pptx/schemas.js +220 -0
  148. package/dist/edit/pptx/session.d.ts +54 -0
  149. package/dist/edit/pptx/session.js +145 -0
  150. package/dist/edit/pptx/shape-ops.d.ts +10 -0
  151. package/dist/edit/pptx/shape-ops.js +486 -0
  152. package/dist/edit/pptx/slide-ops.d.ts +6 -0
  153. package/dist/edit/pptx/slide-ops.js +353 -0
  154. package/dist/edit/pptx/style.d.ts +31 -0
  155. package/dist/edit/pptx/style.js +181 -0
  156. package/dist/edit/pptx/text-ops.d.ts +13 -0
  157. package/dist/edit/pptx/text-ops.js +267 -0
  158. package/dist/edit/pptx/text-write.d.ts +67 -0
  159. package/dist/edit/pptx/text-write.js +293 -0
  160. package/dist/edit/pptx/text.d.ts +41 -0
  161. package/dist/edit/pptx/text.js +99 -0
  162. package/dist/edit/pptx/types.d.ts +232 -0
  163. package/dist/edit/pptx/types.js +1 -0
  164. package/dist/edit/schema.d.ts +11 -0
  165. package/dist/edit/schema.js +275 -0
  166. package/dist/edit/session.d.ts +77 -0
  167. package/dist/edit/session.js +943 -0
  168. package/dist/edit/sessions.d.ts +8 -0
  169. package/dist/edit/sessions.js +1 -0
  170. package/dist/edit/types.d.ts +290 -0
  171. package/dist/edit/types.js +1 -0
  172. package/dist/edit/worker-engine.d.ts +31 -0
  173. package/dist/edit/worker-engine.js +91 -0
  174. package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
  175. package/dist/fonts/manifest.json +6 -1
  176. package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
  177. package/dist/fonts.d.ts +2 -0
  178. package/dist/fonts.js +4 -0
  179. package/dist/fuzzy-alignment.d.ts +11 -4
  180. package/dist/fuzzy-alignment.js +3 -9
  181. package/dist/headless.d.ts +5 -1
  182. package/dist/headless.js +8 -1
  183. package/dist/index.d.ts +10 -2
  184. package/dist/index.js +16 -2
  185. package/dist/limits.js +6 -0
  186. package/dist/ooxml-edit-worker.d.ts +1 -0
  187. package/dist/ooxml-edit-worker.js +8 -0
  188. package/dist/pdf-edit-worker.d.ts +1 -0
  189. package/dist/pdf-edit-worker.js +35 -0
  190. package/dist/spreadsheet-viewport.d.ts +5 -0
  191. package/dist/spreadsheet-viewport.js +11 -0
  192. package/dist/ui.js +7 -0
  193. package/dist/viewer.d.ts +6 -0
  194. package/dist/viewer.js +293 -19
  195. package/dist/viewport.d.ts +13 -0
  196. package/dist/viewport.js +126 -1
  197. package/dist/worker-protocol.d.ts +39 -2
  198. package/dist/workers/fuzzy-search-worker.js +1 -1
  199. package/dist/workers/ooxml-edit-worker.js +9212 -0
  200. package/dist/workers/pdf-edit-worker.js +10847 -0
  201. package/package.json +3 -3
@@ -0,0 +1,529 @@
1
+ import { resolveBinary } from "../assets.js";
2
+ import { extensionForMime, IMAGE_RELATIONSHIP_TYPE } from "../ooxml/opc.js";
3
+ import { patches } from "../ooxml/patch.js";
4
+ import { relativeTarget } from "../ooxml/transaction.js";
5
+ import { OFFICE_RELATIONSHIPS, W_NS } from "./ids.js";
6
+ import { firstTextItem } from "./text.js";
7
+ import { deletedParagraphXml, insertedRunXml, markedParagraphProperties, trackedRangeProblem, unsupportedTracked, } from "./tracked.js";
8
+ import { attributeProblem, changedRunProperties, colorProblem, namespacePatches, normalizeText, paragraphPropertiesWithoutSection, paragraphXml, runContentXml, runXml, sliceOf, textProblem, } from "./write.js";
9
+ /*
10
+ * Structure: paragraphs inserted next to other elements, paragraphs,
11
+ * tables and inline pictures deleted, paragraphs and tables moved within
12
+ * their container, inline pictures inserted as a paragraph of their own.
13
+ * Every operation is a patch of the body part; a moved or new paragraph
14
+ * carries its id, so ids survive the move.
15
+ */
16
+ const MEDIA_FOLDER = "/word/media";
17
+ const WP_NS = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
18
+ const A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main";
19
+ const PIC_NS = "http://schemas.openxmlformats.org/drawingml/2006/picture";
20
+ const R_NS = OFFICE_RELATIONSHIPS.slice(0, -1);
21
+ /** EMU per point. */
22
+ const EMU_PER_PT = 12_700;
23
+ /** The node a block occupies among its siblings: the paragraph or table itself. */
24
+ function blockNode(record) {
25
+ return record.node;
26
+ }
27
+ /** Exactly one of `before` and `after`, resolved to its record. */
28
+ function placement(operation, context, issue) {
29
+ const named = [operation.before, operation.after].filter((value) => value !== undefined);
30
+ if (named.length !== 1) {
31
+ issue("/before", "invalid-value", "Exactly one of before and after names the reference element");
32
+ return undefined;
33
+ }
34
+ const side = operation.before !== undefined ? "before" : "after";
35
+ const id = operation[side];
36
+ const record = context.model.byId.get(id);
37
+ if (!record || (record.kind !== "paragraph" && record.kind !== "table")) {
38
+ issue(`/${side}`, record ? "invalid-target" : "unknown-target", record
39
+ ? `Element ${id} is not a paragraph or a table`
40
+ : `No element ${id}`);
41
+ return undefined;
42
+ }
43
+ return { record, side };
44
+ }
45
+ /** The `w:rPr` bytes new text next to a reference takes: its first text run's, else its paragraph mark's. */
46
+ function referenceRunProperties(part, record) {
47
+ const paragraph = record.kind === "paragraph" ? record : record.rows[0]?.[0]?.paragraphs[0];
48
+ if (!paragraph)
49
+ return undefined;
50
+ const first = firstTextItem(paragraph.text);
51
+ if (first?.rPr)
52
+ return first.rPr;
53
+ return paragraph.pPr?.children.find((child) => child.local === "rPr" && child.namespace === W_NS);
54
+ }
55
+ /** Ids of every `w:p` under a node, the node included when it is one. */
56
+ function paragraphIdsUnder(model, node) {
57
+ const out = [];
58
+ if (node.local === "p" && node.namespace === W_NS) {
59
+ const own = model.paragraphIds.get(node);
60
+ if (own)
61
+ out.push(own);
62
+ }
63
+ for (const paragraph of model.document.findAll("p", node)) {
64
+ if (paragraph === node || paragraph.namespace !== W_NS)
65
+ continue;
66
+ const id = model.paragraphIds.get(paragraph);
67
+ if (id)
68
+ out.push(id);
69
+ }
70
+ return out;
71
+ }
72
+ /** Element ids of every listed element under a node, for `removedIds`. */
73
+ function elementIdsUnder(model, node) {
74
+ const out = [];
75
+ for (const record of model.records) {
76
+ let current = record.node;
77
+ while (current && current !== node)
78
+ current = current.parent;
79
+ if (current === node)
80
+ out.push(record.elementId);
81
+ }
82
+ return out;
83
+ }
84
+ /**
85
+ * The bytes of a node with a `w14:paraId` written on every paragraph in
86
+ * it that lacks one, the ids being those the engine knows them by, so a
87
+ * copy placed elsewhere keeps the same ids.
88
+ */
89
+ function stampedSlice(model, node) {
90
+ const part = model.document;
91
+ const targets = [
92
+ ...(node.local === "p" && node.namespace === W_NS ? [node] : []),
93
+ ...part
94
+ .findAll("p", node)
95
+ .filter((p) => p !== node && p.namespace === W_NS),
96
+ ]
97
+ .filter((paragraph) => {
98
+ const id = model.paragraphIds.get(paragraph);
99
+ return id !== undefined && model.unauthoredSet.has(id);
100
+ })
101
+ .sort((a, b) => b.start - a.start);
102
+ let xml = sliceOf(part, node);
103
+ const stamped = [];
104
+ for (const paragraph of targets) {
105
+ const id = model.paragraphIds.get(paragraph);
106
+ // A paragraph stamped by a shown base already carries the attribute.
107
+ if (part.attribute(paragraph, "w14:paraId")) {
108
+ stamped.push(id);
109
+ continue;
110
+ }
111
+ // After the attributes of the start tag, before ">" or "/>".
112
+ const last = paragraph.attributes.at(-1);
113
+ const from = last ? last.end : paragraph.start + 1 + paragraph.name.length;
114
+ const close = part.text.indexOf(">", from);
115
+ let at = close;
116
+ if (part.text[close - 1] === "/")
117
+ at = close - 1;
118
+ const offset = at - node.start;
119
+ xml = `${xml.slice(0, offset)} w14:paraId="${id}"${xml.slice(offset)}`;
120
+ stamped.push(id);
121
+ }
122
+ return { xml, stamped: stamped.reverse() };
123
+ }
124
+ /** The table a cell paragraph names and whether it is the table's first paragraph. */
125
+ function firstCellParagraph(record) {
126
+ const table = record.table;
127
+ return table && table.rows[0]?.[0]?.paragraphs[0] === record
128
+ ? table
129
+ : undefined;
130
+ }
131
+ /**
132
+ * A table is named after its first paragraph; when that paragraph
133
+ * changes, the table's id changes with it and the receipt says so.
134
+ */
135
+ function tableRemap(table, newFirstParagraphId) {
136
+ return table.id === newFirstParagraphId
137
+ ? undefined
138
+ : { [table.elementId]: `tbl:${newFirstParagraphId}` };
139
+ }
140
+ function commit(context, items, result, transaction = context.pkg.transaction()) {
141
+ const part = context.model.document;
142
+ transaction.patch(part, [...namespacePatches(part), ...items]);
143
+ return transaction
144
+ .commit()
145
+ .then((change) => ({ ...result, warnings: change.warnings }));
146
+ }
147
+ export const insertParagraphHandler = {
148
+ async validate(operation, context, issue) {
149
+ const problem = textProblem(normalizeText(operation.text));
150
+ if (problem)
151
+ issue("/text", "invalid-text", `The text holds ${problem}`);
152
+ if (operation.style?.color !== undefined) {
153
+ const colour = colorProblem(operation.style.color);
154
+ if (colour)
155
+ issue("/style/color", "invalid-value", `Colour: ${colour}`);
156
+ }
157
+ if (operation.style?.fontFamily !== undefined) {
158
+ const font = attributeProblem(operation.style.fontFamily);
159
+ if (font)
160
+ issue("/style/fontFamily", "invalid-value", `Font: ${font}`);
161
+ }
162
+ placement(operation, context, issue);
163
+ },
164
+ async apply(operation, context) {
165
+ const { record, side } = placement(operation, context, () => { });
166
+ const part = context.model.document;
167
+ const node = blockNode(record);
168
+ const sourceRPr = referenceRunProperties(part, record);
169
+ const rPr = operation.style
170
+ ? changedRunProperties(part, sourceRPr, operation.style, context.model.styles)
171
+ : sliceOf(part, sourceRPr);
172
+ const sourcePPr = record.kind === "paragraph" ? record.pPr : undefined;
173
+ const pPr = paragraphPropertiesWithoutSection(part, sourcePPr);
174
+ const segments = normalizeText(operation.text).split("\n");
175
+ const createdIds = [];
176
+ const xml = segments.map((segment) => {
177
+ const id = context.freshParagraphId();
178
+ createdIds.push(`p:${id}`);
179
+ // Tracked: the paragraph mark and the run are both insertions.
180
+ return context.tracked
181
+ ? paragraphXml(undefined, id, markedParagraphProperties(context, sourcePPr, "ins"), insertedRunXml(context, rPr, segment))
182
+ : paragraphXml(undefined, id, pPr, runXml(rPr, runContentXml(segment)));
183
+ });
184
+ // Inserts at one position land in call order, so the paragraphs
185
+ // keep their order on either side of the reference.
186
+ const items = side === "before"
187
+ ? xml.map((paragraph) => patches.insertBefore(part, node, paragraph))
188
+ : xml.map((paragraph) => patches.insertAfter(part, node, paragraph));
189
+ const table = side === "before" && record.kind === "paragraph"
190
+ ? firstCellParagraph(record)
191
+ : undefined;
192
+ const remappedIds = table
193
+ ? tableRemap(table, createdIds[0].slice(2))
194
+ : undefined;
195
+ return commit(context, items, {
196
+ createdIds,
197
+ reflowFrom: record.id,
198
+ ...(remappedIds ? { remappedIds } : {}),
199
+ });
200
+ },
201
+ };
202
+ /**
203
+ * The patch that keeps a body from ending with a table, or from holding
204
+ * no block at all, once `remaining` are its blocks: an empty paragraph
205
+ * after the last of them, or where the body's section properties start.
206
+ */
207
+ function bodyTailPatch(context, remaining) {
208
+ const { model } = context;
209
+ const last = remaining.at(-1);
210
+ if (last && last.kind === "paragraph")
211
+ return undefined;
212
+ const id = context.freshParagraphId();
213
+ const xml = paragraphXml(undefined, id, "", "");
214
+ const part = model.document;
215
+ const patch = last
216
+ ? patches.insertAfter(part, last.node, xml)
217
+ : model.bodySectPr
218
+ ? patches.insertBefore(part, model.bodySectPr, xml)
219
+ : patches.appendChild(part, model.body, xml);
220
+ return { patch, id };
221
+ }
222
+ /** Whether the container of a block would keep a paragraph without it. */
223
+ function otherParagraphsIn(model, record) {
224
+ if (record.table) {
225
+ for (const row of record.table.rows)
226
+ for (const cell of row)
227
+ if (cell.paragraphs.includes(record))
228
+ return cell.paragraphs.some((candidate) => candidate !== record);
229
+ return false;
230
+ }
231
+ return model.blocks.some((block) => block.kind === "paragraph" && block !== record);
232
+ }
233
+ export const deleteElementHandler = {
234
+ async validate(operation, context, issue) {
235
+ const record = context.model.byId.get(operation.target);
236
+ if (!record || record.kind === "other") {
237
+ issue("/target", record ? "invalid-target" : "unknown-target", record
238
+ ? `Element ${operation.target} cannot be deleted`
239
+ : `No element ${operation.target}`);
240
+ return;
241
+ }
242
+ if (context.tracked && record.kind !== "paragraph") {
243
+ unsupportedTracked(issue, "/target", "Deleting a table or a picture");
244
+ return;
245
+ }
246
+ if (record.kind === "paragraph") {
247
+ if (context.tracked &&
248
+ !record.readOnlyReason &&
249
+ trackedRangeProblem(record, 0, record.text.text.length, issue, "/target"))
250
+ return;
251
+ if (record.sectPr)
252
+ issue("/target", "section-break", "A paragraph that ends a section cannot be deleted");
253
+ else if (record.readOnlyReason)
254
+ issue("/target", "invalid-target", `Paragraph ${operation.target} is read-only (${record.readOnlyReason})`);
255
+ else if (!otherParagraphsIn(context.model, record))
256
+ issue("/target", "last-paragraph", "The last paragraph of a body or a cell cannot be deleted");
257
+ }
258
+ },
259
+ async apply(operation, context) {
260
+ const { model } = context;
261
+ const found = model.byId.get(operation.target);
262
+ const part = model.document;
263
+ if (found.kind !== "paragraph" && found.kind !== "table")
264
+ return deleteInline(context, found);
265
+ const record = found;
266
+ const node = blockNode(record);
267
+ if (context.tracked && record.kind === "paragraph") {
268
+ // The paragraph stays, marked deleted; its inline objects leave
269
+ // the model with their runs, Word drops the rest on accept.
270
+ const nested = paragraphIdsUnder(model, node).filter((id) => id !== record.id);
271
+ return commit(context, [
272
+ patches.replaceElement(part, node, deletedParagraphXml(context, record)),
273
+ ], {
274
+ createdIds: [],
275
+ removedIds: record.inlines.map((inline) => inline.elementId),
276
+ ...(nested.length > 0 ? { removedParagraphIds: nested } : {}),
277
+ ...(model.unauthoredSet.has(record.id)
278
+ ? { stamped: [record.id] }
279
+ : {}),
280
+ reflowFrom: record.id,
281
+ });
282
+ }
283
+ const items = [patches.removeElement(part, node)];
284
+ const createdIds = [];
285
+ if (model.blocks.includes(record)) {
286
+ // The body must not end with a table or stand empty.
287
+ const tail = bodyTailPatch(context, model.blocks.filter((block) => block !== record));
288
+ if (tail) {
289
+ createdIds.push(`p:${tail.id}`);
290
+ items.push(tail.patch);
291
+ }
292
+ }
293
+ const removedParagraphIds = paragraphIdsUnder(model, node);
294
+ const table = record.kind === "paragraph" ? firstCellParagraph(record) : undefined;
295
+ const successor = table?.rows[0]?.[0]?.paragraphs[1];
296
+ const remappedIds = table && successor ? tableRemap(table, successor.id) : undefined;
297
+ return commit(context, items, {
298
+ createdIds,
299
+ removedIds: elementIdsUnder(model, node),
300
+ removedParagraphIds,
301
+ reflowFrom: record.id,
302
+ ...(remappedIds ? { remappedIds } : {}),
303
+ });
304
+ },
305
+ };
306
+ /** Removes an inline object's drawing and the relationship only it used. */
307
+ async function deleteInline(context, record) {
308
+ const { model } = context;
309
+ const part = model.document;
310
+ const transaction = context.pkg.transaction();
311
+ const blip = part.find("blip", record.node);
312
+ const rId = blip
313
+ ? (part.attribute(blip, "r:embed") ?? part.attribute(blip, "r:link"))
314
+ : undefined;
315
+ if (rId) {
316
+ const uses = part
317
+ .findAll("blip")
318
+ .filter((candidate) => candidate !== blip &&
319
+ (part.attribute(candidate, "r:embed") === rId ||
320
+ part.attribute(candidate, "r:link") === rId));
321
+ const elsewhere = new RegExp(`r:(?:id|embed|link|pict)="${rId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`);
322
+ const others = uses.length > 0 || elsewhere.test(stripNode(part, record.node));
323
+ if (!others)
324
+ transaction.removeRelationship(model.mainPart, rId);
325
+ }
326
+ const removedParagraphIds = paragraphIdsUnder(model, record.node);
327
+ return commit(context, [patches.removeElement(part, record.node)], {
328
+ createdIds: [],
329
+ removedIds: [record.elementId],
330
+ ...(removedParagraphIds.length > 0 ? { removedParagraphIds } : {}),
331
+ reflowFrom: record.paragraph.id,
332
+ }, transaction);
333
+ }
334
+ /** The part's text without a node, to look for other uses of a relationship. */
335
+ function stripNode(part, node) {
336
+ return part.text.slice(0, node.start) + part.text.slice(node.end);
337
+ }
338
+ export const moveElementHandler = {
339
+ async validate(operation, context, issue) {
340
+ if (context.tracked) {
341
+ unsupportedTracked(issue, "", "Moving an element");
342
+ return;
343
+ }
344
+ const record = context.model.byId.get(operation.target);
345
+ if (!record || (record.kind !== "paragraph" && record.kind !== "table")) {
346
+ issue("/target", record ? "invalid-target" : "unknown-target", record
347
+ ? `Element ${operation.target} is not a paragraph or a table`
348
+ : `No element ${operation.target}`);
349
+ return;
350
+ }
351
+ if (record.kind === "paragraph" && record.sectPr) {
352
+ issue("/target", "section-break", "A paragraph that ends a section cannot move");
353
+ return;
354
+ }
355
+ if (record.kind === "paragraph" && record.readOnlyReason) {
356
+ issue("/target", "invalid-target", `Paragraph ${operation.target} is read-only (${record.readOnlyReason})`);
357
+ return;
358
+ }
359
+ const reference = placement(operation, context, issue);
360
+ if (!reference)
361
+ return;
362
+ if (reference.record === record) {
363
+ issue(`/${reference.side}`, "invalid-target", "An element cannot move next to itself");
364
+ return;
365
+ }
366
+ if (blockNode(reference.record).parent !== blockNode(record).parent)
367
+ issue(`/${reference.side}`, "invalid-target", "The reference must be in the same body or cell as the element");
368
+ },
369
+ async apply(operation, context) {
370
+ const { model } = context;
371
+ const record = model.byId.get(operation.target);
372
+ const { record: reference, side } = placement(operation, context, () => { });
373
+ const part = model.document;
374
+ const node = blockNode(record);
375
+ const target = blockNode(reference);
376
+ const { xml, stamped } = stampedSlice(model, node);
377
+ const items = [
378
+ patches.removeElement(part, node),
379
+ side === "before"
380
+ ? patches.insertBefore(part, target, xml)
381
+ : patches.insertAfter(part, target, xml),
382
+ ];
383
+ const createdIds = [];
384
+ if (model.blocks.includes(record) && model.blocks.includes(reference)) {
385
+ // The body must not end with a table once the blocks are reordered.
386
+ const without = model.blocks.filter((block) => block !== record);
387
+ const at = without.indexOf(reference);
388
+ without.splice(side === "before" ? at : at + 1, 0, record);
389
+ const tail = bodyTailPatch(context, without);
390
+ if (tail) {
391
+ createdIds.push(`p:${tail.id}`);
392
+ items.push(tail.patch);
393
+ }
394
+ }
395
+ // The document reflows from whichever of the two comes first.
396
+ const order = (candidate) => model.records.indexOf(candidate);
397
+ const first = order(record) < order(reference) ? record : reference;
398
+ // Within a table's first cell, the move may change which paragraph
399
+ // comes first, and so the table's id.
400
+ let remappedIds;
401
+ if (record.kind === "paragraph" && record.table) {
402
+ const cell = record.table.rows[0]?.[0];
403
+ if (cell && cell.paragraphs.includes(record)) {
404
+ const without = cell.paragraphs.filter((p) => p !== record);
405
+ const at = without.indexOf(reference);
406
+ const reordered = [...without];
407
+ if (at >= 0)
408
+ reordered.splice(side === "before" ? at : at + 1, 0, record);
409
+ const head = reordered[0];
410
+ if (head)
411
+ remappedIds = tableRemap(record.table, head.id);
412
+ }
413
+ }
414
+ return commit(context, items, {
415
+ createdIds,
416
+ stamped,
417
+ reflowFrom: first.id,
418
+ ...(remappedIds ? { remappedIds } : {}),
419
+ });
420
+ },
421
+ };
422
+ /** A media part of the package with these bytes and this type's extension, if any. */
423
+ async function existingMedia(context, bytes, mimeType) {
424
+ const extension = `.${extensionForMime(mimeType)}`;
425
+ for (const name of context.pkg.currentPartNames) {
426
+ if (!name.startsWith(`${MEDIA_FOLDER}/`) || !name.endsWith(extension))
427
+ continue;
428
+ const entry = context.pkg.entry(name);
429
+ if (entry && entry.uncompressedSize !== bytes.length)
430
+ continue;
431
+ const candidate = await context.pkg.part(name);
432
+ if (candidate.length !== bytes.length)
433
+ continue;
434
+ let same = true;
435
+ for (let index = 0; index < bytes.length; index += 1)
436
+ if (candidate[index] !== bytes[index]) {
437
+ same = false;
438
+ break;
439
+ }
440
+ if (same)
441
+ return name;
442
+ }
443
+ return undefined;
444
+ }
445
+ /** Whether the bytes carry the signature of the declared image type. */
446
+ function matchesMime(bytes, mimeType) {
447
+ if (mimeType === "image/png")
448
+ return (bytes.length >= 8 &&
449
+ bytes[0] === 0x89 &&
450
+ bytes[1] === 0x50 &&
451
+ bytes[2] === 0x4e &&
452
+ bytes[3] === 0x47);
453
+ return (bytes.length >= 3 &&
454
+ bytes[0] === 0xff &&
455
+ bytes[1] === 0xd8 &&
456
+ bytes[2] === 0xff);
457
+ }
458
+ /** One above every `wp:docPr/@id` of the part. */
459
+ function nextDrawingId(part) {
460
+ let max = 0;
461
+ for (const node of part.findAll("docPr")) {
462
+ if (node.namespace !== WP_NS)
463
+ continue;
464
+ const id = Number(part.attribute(node, "id") ?? NaN);
465
+ if (Number.isInteger(id))
466
+ max = Math.max(max, id);
467
+ }
468
+ return max + 1;
469
+ }
470
+ /** Patches that declare the namespaces an inline picture's markup uses on the root. */
471
+ function drawingNamespacePatches(part) {
472
+ const root = part.root;
473
+ const has = (name) => root.attributes.some((attribute) => attribute.name === name);
474
+ const items = [];
475
+ if (!has("xmlns:wp"))
476
+ items.push(patches.setAttribute(part, root, "xmlns:wp", WP_NS));
477
+ if (!has("xmlns:r"))
478
+ items.push(patches.setAttribute(part, root, "xmlns:r", R_NS));
479
+ return items;
480
+ }
481
+ export const insertImageHandler = {
482
+ async validate(operation, context, issue) {
483
+ if (context.tracked) {
484
+ unsupportedTracked(issue, "", "Inserting a picture");
485
+ return;
486
+ }
487
+ placement(operation, context, issue);
488
+ try {
489
+ const bytes = resolveBinary(operation.data, context.assets);
490
+ if (bytes.length === 0)
491
+ issue("/data", "invalid-value", "The image has no bytes");
492
+ else if (!matchesMime(bytes, operation.mimeType))
493
+ issue("/data", "invalid-value", `The bytes are not a ${operation.mimeType} image`);
494
+ }
495
+ catch {
496
+ issue("/data", "unknown-asset", "The asset is not known to the session");
497
+ }
498
+ },
499
+ async apply(operation, context) {
500
+ const { model } = context;
501
+ const { record, side } = placement(operation, context, () => { });
502
+ const part = model.document;
503
+ const bytes = resolveBinary(operation.data, context.assets);
504
+ const transaction = context.pkg.transaction();
505
+ // The same bytes already in the package are related, not stored again.
506
+ const existing = await existingMedia(context, bytes, operation.mimeType);
507
+ const rId = existing
508
+ ? await transaction.addRelationship(model.mainPart, IMAGE_RELATIONSHIP_TYPE, relativeTarget(model.mainPart, existing))
509
+ : (await transaction.addMedia(model.mainPart, MEDIA_FOLDER, bytes, operation.mimeType)).rId;
510
+ const cx = Math.round(operation.size.width * EMU_PER_PT);
511
+ const cy = Math.round(operation.size.height * EMU_PER_PT);
512
+ const drawingId = nextDrawingId(part);
513
+ const id = context.freshParagraphId();
514
+ const drawing = `<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/><wp:effectExtent l="0" t="0" r="0" b="0"/><wp:docPr id="${drawingId}" name="Picture ${drawingId}"/>` +
515
+ `<wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="${A_NS}" noChangeAspect="1"/></wp:cNvGraphicFramePr>` +
516
+ `<a:graphic xmlns:a="${A_NS}"><a:graphicData uri="${PIC_NS}"><pic:pic xmlns:pic="${PIC_NS}"><pic:nvPicPr><pic:cNvPr id="0" name="Picture ${drawingId}"/><pic:cNvPicPr/></pic:nvPicPr>` +
517
+ `<pic:blipFill><a:blip r:embed="${rId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>` +
518
+ `<pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
519
+ const xml = paragraphXml(undefined, id, "", drawing);
520
+ const node = blockNode(record);
521
+ const items = [
522
+ ...drawingNamespacePatches(part),
523
+ side === "before"
524
+ ? patches.insertBefore(part, node, xml)
525
+ : patches.insertAfter(part, node, xml),
526
+ ];
527
+ return commit(context, items, { createdIds: [`p:${id}`, `img:${id}.0`], reflowFrom: record.id }, transaction);
528
+ },
529
+ };
@@ -0,0 +1,45 @@
1
+ import type { OoxmlPackage } from "../ooxml/package.js";
2
+ import type { XmlElement, XmlPart } from "../ooxml/xml.js";
3
+ import type { DocxParagraphStyle, DocxTextAlign, DocxTextStyle } from "./types.js";
4
+ export interface StyleRecord {
5
+ readonly id: string;
6
+ readonly type: string;
7
+ readonly basedOn?: string;
8
+ readonly isDefault: boolean;
9
+ readonly rPr?: XmlElement;
10
+ readonly pPr?: XmlElement;
11
+ }
12
+ export interface ThemeFonts {
13
+ readonly major: string;
14
+ readonly minor: string;
15
+ }
16
+ /** `#RRGGBB` per colour scheme slot (`dk1`, `lt1`, `accent1`, …). */
17
+ export type ThemeColors = ReadonlyMap<string, string>;
18
+ /** Whether a name is one Word accepts in `w:themeColor`. */
19
+ export declare function isThemeColorName(name: string): boolean;
20
+ /** The styles and theme of a document, read once per revision. */
21
+ export declare class DocxStyles {
22
+ readonly part: XmlPart | undefined;
23
+ readonly styles: ReadonlyMap<string, StyleRecord>;
24
+ readonly defaultParagraph: StyleRecord | undefined;
25
+ readonly defaultCharacter: StyleRecord | undefined;
26
+ readonly docDefaultsRPr: XmlElement | undefined;
27
+ readonly docDefaultsPPr: XmlElement | undefined;
28
+ readonly fonts: ThemeFonts;
29
+ readonly colors: ThemeColors;
30
+ private constructor();
31
+ /** The `#RRGGBB` a `w:themeColor` name resolves to in this theme, when it has one. */
32
+ themeColor(name: string): string | undefined;
33
+ static load(pkg: OoxmlPackage, documentPart: string, signal?: AbortSignal): Promise<DocxStyles>;
34
+ /** A style and the styles it is based on, nearest first; cycles stop. */
35
+ chain(id: string | undefined): StyleRecord[];
36
+ }
37
+ /** `w:b`, `w:i`, `w:default`-style toggles: present without a value means on. */
38
+ export declare function isOn(value: string | undefined): boolean;
39
+ /** The run property holders in resolution order for a run of a paragraph. */
40
+ export declare function runPropertySources(styles: DocxStyles, pPr: XmlElement | undefined, rPr: XmlElement | undefined): readonly XmlElement[];
41
+ /** The paragraph property holders in resolution order. */
42
+ export declare function paragraphPropertySources(styles: DocxStyles, pPr: XmlElement | undefined): readonly XmlElement[];
43
+ export declare function resolveTextStyle(styles: DocxStyles, pPr: XmlElement | undefined, rPr: XmlElement | undefined): DocxTextStyle;
44
+ export declare function alignOf(value: string | undefined): DocxTextAlign | undefined;
45
+ export declare function resolveParagraphStyle(styles: DocxStyles, pPr: XmlElement | undefined): DocxParagraphStyle;