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,114 @@
1
+ import { ViewerError } from "../../errors.js";
2
+ /**
3
+ * The PPTX session: the core session narrowed to PPTX operations and
4
+ * elements, plus the slide and layout reads. Each typed method is `apply()`
5
+ * with a single operation.
6
+ */
7
+ export class PptxSession {
8
+ format = "pptx";
9
+ #core;
10
+ constructor(core) {
11
+ this.#core = core;
12
+ }
13
+ get sessionId() {
14
+ return this.#core.sessionId;
15
+ }
16
+ get state() {
17
+ return this.#core.state;
18
+ }
19
+ get schemas() {
20
+ return this.#core.schemas;
21
+ }
22
+ apply(operations, options) {
23
+ return this.#core.apply(operations, options);
24
+ }
25
+ applyJson(operations, options) {
26
+ return this.#core.applyJson(operations, options);
27
+ }
28
+ undo(options) {
29
+ return this.#core.undo(options);
30
+ }
31
+ redo(options) {
32
+ return this.#core.redo(options);
33
+ }
34
+ reset(options) {
35
+ return this.#core.reset(options);
36
+ }
37
+ save(options) {
38
+ return this.#core.save(options);
39
+ }
40
+ markSaved(stateToken) {
41
+ this.#core.markSaved(stateToken);
42
+ }
43
+ addAsset(data, options) {
44
+ return this.#core.addAsset(data, options);
45
+ }
46
+ getElements(query, options) {
47
+ return this.#core.getElements(query, options);
48
+ }
49
+ getElement(id, options) {
50
+ return this.#core.getElement(id, options);
51
+ }
52
+ elementsAt(pageIndex, point, options) {
53
+ return this.#core.elementsAt(pageIndex, point, options);
54
+ }
55
+ findText(query, options) {
56
+ return this.#core.findText(query, options);
57
+ }
58
+ getSlides(options) {
59
+ return this.#core.readItems(options, (engine, signal) => pptxReads(engine).slides(signal));
60
+ }
61
+ getLayouts(options) {
62
+ return this.#core.readItems(options, (engine, signal) => pptxReads(engine).layouts(signal));
63
+ }
64
+ replaceText(fields, options) {
65
+ return this.apply([{ op: "replaceText", ...fields }], options);
66
+ }
67
+ setTextStyle(fields, options) {
68
+ return this.apply([{ op: "setTextStyle", ...fields }], options);
69
+ }
70
+ setShapeStyle(fields, options) {
71
+ return this.apply([{ op: "setShapeStyle", ...fields }], options);
72
+ }
73
+ moveElement(fields, options) {
74
+ return this.apply([{ op: "moveElement", ...fields }], options);
75
+ }
76
+ resizeElement(fields, options) {
77
+ return this.apply([{ op: "resizeElement", ...fields }], options);
78
+ }
79
+ deleteElement(fields, options) {
80
+ return this.apply([{ op: "deleteElement", ...fields }], options);
81
+ }
82
+ insertTextBox(fields, options) {
83
+ return this.apply([{ op: "insertTextBox", ...fields }], options);
84
+ }
85
+ insertImage(fields, options) {
86
+ return this.apply([{ op: "insertImage", ...fields }], options);
87
+ }
88
+ insertTable(fields, options) {
89
+ return this.apply([{ op: "insertTable", ...fields }], options);
90
+ }
91
+ setTableCell(fields, options) {
92
+ return this.apply([{ op: "setTableCell", ...fields }], options);
93
+ }
94
+ insertSlide(fields, options) {
95
+ return this.apply([{ op: "insertSlide", ...fields }], options);
96
+ }
97
+ duplicateSlide(fields, options) {
98
+ return this.apply([{ op: "duplicateSlide", ...fields }], options);
99
+ }
100
+ deleteSlide(fields, options) {
101
+ return this.apply([{ op: "deleteSlide", ...fields }], options);
102
+ }
103
+ moveSlide(fields, options) {
104
+ return this.apply([{ op: "moveSlide", ...fields }], options);
105
+ }
106
+ }
107
+ /** The engine's PPTX reads; an engine without them cannot serve this session. */
108
+ function pptxReads(engine) {
109
+ const candidate = engine;
110
+ if (typeof candidate.slides !== "function" ||
111
+ typeof candidate.layouts !== "function")
112
+ throw new ViewerError("edit-unsupported", "The engine does not provide the PPTX reads", { details: { format: "pptx", reason: "no-reads" } });
113
+ return candidate;
114
+ }
@@ -0,0 +1,10 @@
1
+ import type { XmlPart } from "../ooxml/xml.js";
2
+ import { type PptxOperationHandler } from "./operations.js";
3
+ import type { PptxDeleteElementOperation, PptxInsertTextBoxOperation, PptxMoveElementOperation, PptxResizeElementOperation, PptxSetShapeStyleOperation } from "./types.js";
4
+ export declare const moveElementHandler: PptxOperationHandler<PptxMoveElementOperation>;
5
+ export declare const resizeElementHandler: PptxOperationHandler<PptxResizeElementOperation>;
6
+ export declare const deleteElementHandler: PptxOperationHandler<PptxDeleteElementOperation>;
7
+ export declare const setShapeStyleHandler: PptxOperationHandler<PptxSetShapeStyleOperation>;
8
+ /** Paragraph XML for a text and a style, as a new text box or table cell carries it. */
9
+ export declare function paragraphsXml(part: XmlPart, text: string, style: PptxInsertTextBoxOperation["style"]): string;
10
+ export declare const insertTextBoxHandler: PptxOperationHandler<PptxInsertTextBoxOperation>;
@@ -0,0 +1,486 @@
1
+ import { patches } from "../ooxml/patch.js";
2
+ import { apply as applyMatrix, invert, pxToEmu, } from "./geometry.js";
3
+ import { committedParts, } from "./operations.js";
4
+ import { alignValue, changedRunProperties, colorProblem, itemsOfSegment, normalizeText, paragraphXml, solidFillXml, textProblem, PARAGRAPH_BREAK, } from "./text-write.js";
5
+ /*
6
+ * Shape operations: fill and line in p:spPr, frames written to a:xfrm (an
7
+ * inheriting placeholder gets an explicit one), deletion with the slide's
8
+ * relationships only the element used, and text boxes appended to the
9
+ * shape tree.
10
+ */
11
+ const FILLS = new Set([
12
+ "noFill",
13
+ "solidFill",
14
+ "gradFill",
15
+ "blipFill",
16
+ "pattFill",
17
+ "grpFill",
18
+ ]);
19
+ /** Children of p:spPr that follow the fill. */
20
+ const AFTER_SHAPE_FILL = new Set([
21
+ "ln",
22
+ "effectLst",
23
+ "effectDag",
24
+ "scene3d",
25
+ "sp3d",
26
+ "extLst",
27
+ ]);
28
+ /** Children of p:spPr that follow a:ln. */
29
+ const AFTER_LINE = new Set([
30
+ "effectLst",
31
+ "effectDag",
32
+ "scene3d",
33
+ "sp3d",
34
+ "extLst",
35
+ ]);
36
+ async function editableTarget(id, context, issue) {
37
+ const record = await context.locate(id);
38
+ if (!record) {
39
+ issue("/target", "unknown-target", `No element ${id}`);
40
+ return undefined;
41
+ }
42
+ if (record.readOnly) {
43
+ issue("/target", "invalid-target", `Element ${id} is read-only`);
44
+ return undefined;
45
+ }
46
+ return record;
47
+ }
48
+ function sliceOf(part, node) {
49
+ return part.text.slice(node.start, node.end);
50
+ }
51
+ function commit(context, record, items, extra = {}) {
52
+ const transaction = context.pkg.transaction();
53
+ transaction.patch(record.part, items);
54
+ for (const id of extra.relationships ?? [])
55
+ transaction.removeRelationship(record.slide.part, id);
56
+ return transaction.commit().then((change) => ({
57
+ createdIds: extra.createdIds ?? [],
58
+ ...(extra.removedIds ? { removedIds: extra.removedIds } : {}),
59
+ changedPages: [record.element.pageIndex],
60
+ warnings: [...(extra.warnings ?? []), ...change.warnings],
61
+ parts: committedParts(change),
62
+ }));
63
+ }
64
+ /** The node whose `a:xfrm` positions the element: p:spPr, p:grpSpPr or the frame itself. */
65
+ function frameHolder(record) {
66
+ switch (record.node.local) {
67
+ case "graphicFrame":
68
+ return record.node;
69
+ case "grpSp":
70
+ return record.node.children.find((child) => child.local === "grpSpPr");
71
+ default:
72
+ return record.spPr;
73
+ }
74
+ }
75
+ /** Patches that give the element the frame, creating `a:xfrm` when it inherits one. */
76
+ function framePatches(record, frame) {
77
+ const { part } = record;
78
+ const xfrm = record.xfrmNode;
79
+ if (xfrm) {
80
+ const off = xfrm.children.find((child) => child.local === "off");
81
+ const ext = xfrm.children.find((child) => child.local === "ext");
82
+ if (off && ext)
83
+ return [
84
+ patches.setAttribute(part, off, "x", String(frame.x)),
85
+ patches.setAttribute(part, off, "y", String(frame.y)),
86
+ patches.setAttribute(part, ext, "cx", String(frame.cx)),
87
+ patches.setAttribute(part, ext, "cy", String(frame.cy)),
88
+ ];
89
+ }
90
+ const tag = record.node.local === "graphicFrame" ? "p:xfrm" : "a:xfrm";
91
+ const xml = `<${tag}><a:off x="${frame.x}" y="${frame.y}"/><a:ext cx="${frame.cx}" cy="${frame.cy}"/></${tag}>`;
92
+ const holder = frameHolder(record);
93
+ if (!holder) {
94
+ // A shape without p:spPr: add one where the schema puts it.
95
+ return [
96
+ insertShapeProperties(part, record.node, `<p:spPr>${xml}</p:spPr>`),
97
+ ];
98
+ }
99
+ if (xfrm)
100
+ return [patches.replaceElement(part, xfrm, xml)];
101
+ if (record.node.local === "graphicFrame") {
102
+ // p:xfrm follows p:nvGraphicFramePr and precedes a:graphic.
103
+ const graphic = holder.children.find((child) => child.local === "graphic");
104
+ return [
105
+ graphic
106
+ ? patches.insertBefore(part, graphic, xml)
107
+ : patches.appendChild(part, holder, xml),
108
+ ];
109
+ }
110
+ const first = holder.children[0];
111
+ if (first)
112
+ return [patches.insertBefore(part, first, xml)];
113
+ if (holder.selfClosing)
114
+ return [patches.replaceContent(part, holder, xml)];
115
+ return [patches.appendChild(part, holder, xml)];
116
+ }
117
+ /** A patch that adds `p:spPr` after the non-visual properties, before p:style and the text body. */
118
+ function insertShapeProperties(part, node, xml) {
119
+ const next = node.children.find((child) => child.local === "style" || child.local === "txBody");
120
+ return next
121
+ ? patches.insertBefore(part, next, xml)
122
+ : patches.appendChild(part, node, xml);
123
+ }
124
+ /** The element's current frame in its own space, in EMU. */
125
+ function currentFrame(record) {
126
+ return record.xfrm
127
+ ? {
128
+ x: record.xfrm.x,
129
+ y: record.xfrm.y,
130
+ cx: record.xfrm.cx,
131
+ cy: record.xfrm.cy,
132
+ }
133
+ : undefined;
134
+ }
135
+ /** A slide-space offset mapped into the element's space through its groups. */
136
+ function offsetInElementSpace(parents, dx, dy) {
137
+ const inverse = invert(parents);
138
+ return {
139
+ x: inverse.a * dx + inverse.c * dy,
140
+ y: inverse.b * dx + inverse.d * dy,
141
+ };
142
+ }
143
+ /** The element-space frame whose rotated, group-mapped box fills `rect` (slide px). */
144
+ function frameForBounds(record, rect) {
145
+ const placed = record.placed;
146
+ const rotation = (placed.frame.rotation * Math.PI) / 180;
147
+ const cos = Math.abs(Math.cos(rotation));
148
+ const sin = Math.abs(Math.sin(rotation));
149
+ const det = cos * cos - sin * sin;
150
+ let width;
151
+ let height;
152
+ if (Math.abs(det) < 1e-6) {
153
+ // At 45° the box does not determine both sides; keep the aspect ratio.
154
+ const scale = placed.bounds.width > 0 ? rect.width / placed.bounds.width : 1;
155
+ width = placed.frame.width * scale;
156
+ height = placed.frame.height * scale;
157
+ }
158
+ else {
159
+ width = (rect.width * cos - rect.height * sin) / det;
160
+ height = (rect.height * cos - rect.width * sin) / det;
161
+ }
162
+ width = Math.max(width, 0);
163
+ height = Math.max(height, 0);
164
+ const scaleX = Math.hypot(record.parents.a, record.parents.b) || 1;
165
+ const scaleY = Math.hypot(record.parents.c, record.parents.d) || 1;
166
+ const centre = applyMatrix(invert(record.parents), {
167
+ x: pxToEmu(rect.x + rect.width / 2),
168
+ y: pxToEmu(rect.y + rect.height / 2),
169
+ });
170
+ const cx = Math.round(pxToEmu(width) / scaleX);
171
+ const cy = Math.round(pxToEmu(height) / scaleY);
172
+ return {
173
+ x: Math.round(centre.x - cx / 2),
174
+ y: Math.round(centre.y - cy / 2),
175
+ cx,
176
+ cy,
177
+ };
178
+ }
179
+ async function framedTarget(id, context, issue) {
180
+ const record = await editableTarget(id, context, issue);
181
+ if (!record)
182
+ return undefined;
183
+ if (!record.xfrm || !record.placed) {
184
+ issue("/target", "invalid-target", `Element ${id} has no frame`);
185
+ return undefined;
186
+ }
187
+ return record;
188
+ }
189
+ export const moveElementHandler = {
190
+ async validate(operation, context, issue) {
191
+ if (operation.to === undefined && operation.by === undefined)
192
+ issue("/to", "required", "One of to and by is required");
193
+ if (operation.to !== undefined && operation.by !== undefined)
194
+ issue("/by", "conflict", "Only one of to and by may be given");
195
+ await framedTarget(operation.target, context, issue);
196
+ },
197
+ async apply(operation, context) {
198
+ const record = (await framedTarget(operation.target, context, () => { }));
199
+ const bounds = record.placed.bounds;
200
+ const dx = operation.to ? operation.to.x - bounds.x : operation.by.dx;
201
+ const dy = operation.to ? operation.to.y - bounds.y : operation.by.dy;
202
+ const delta = offsetInElementSpace(record.parents, pxToEmu(dx), pxToEmu(dy));
203
+ const own = currentFrame(record);
204
+ return commit(context, record, framePatches(record, {
205
+ ...own,
206
+ x: Math.round(own.x + delta.x),
207
+ y: Math.round(own.y + delta.y),
208
+ }));
209
+ },
210
+ };
211
+ export const resizeElementHandler = {
212
+ async validate(operation, context, issue) {
213
+ const record = await framedTarget(operation.target, context, issue);
214
+ if (!record)
215
+ return;
216
+ const frame = frameForBounds(record, operation.rect);
217
+ if (!(frame.cx > 0 && frame.cy > 0))
218
+ issue("/rect", "invalid-value", "The box leaves no size for the rotated frame");
219
+ },
220
+ async apply(operation, context) {
221
+ const record = (await framedTarget(operation.target, context, () => { }));
222
+ // A group scales its children with its extent; its child space stays.
223
+ return commit(context, record, framePatches(record, frameForBounds(record, operation.rect)));
224
+ },
225
+ };
226
+ /* Deletion */
227
+ /** Relationship ids referenced by attributes in the `r:` namespace under a node. */
228
+ function relationshipIdsIn(part, node) {
229
+ const ids = new Set();
230
+ const stack = [node];
231
+ while (stack.length > 0) {
232
+ const current = stack.pop();
233
+ for (const attribute of current.attributes)
234
+ if (attribute.name.startsWith("r:") && attribute.value)
235
+ ids.add(attribute.value);
236
+ for (const child of current.children)
237
+ stack.push(child);
238
+ }
239
+ return ids;
240
+ }
241
+ function descendantIds(elements, record) {
242
+ const ids = [];
243
+ for (const candidate of elements.records) {
244
+ let parent = candidate.parent;
245
+ while (parent) {
246
+ if (parent === record) {
247
+ ids.push(candidate.element.id);
248
+ break;
249
+ }
250
+ parent = parent.parent;
251
+ }
252
+ }
253
+ return ids;
254
+ }
255
+ export const deleteElementHandler = {
256
+ async validate(operation, context, issue) {
257
+ await editableTarget(operation.target, context, issue);
258
+ },
259
+ async apply(operation, context) {
260
+ const record = (await editableTarget(operation.target, context, () => { }));
261
+ const elements = await context.elements(record.element.pageIndex);
262
+ const { part } = record;
263
+ const inside = relationshipIdsIn(part, record.node);
264
+ const outside = relationshipIdsIn(part, part.root);
265
+ // Ids the rest of the part still uses stay; count their uses outside the element.
266
+ const exclusive = [...inside].filter((id) => {
267
+ let uses = 0;
268
+ const stack = [part.root];
269
+ while (stack.length > 0) {
270
+ const current = stack.pop();
271
+ if (current === record.node)
272
+ continue;
273
+ for (const attribute of current.attributes)
274
+ if (attribute.name.startsWith("r:") && attribute.value === id)
275
+ uses += 1;
276
+ for (const child of current.children)
277
+ stack.push(child);
278
+ }
279
+ return uses === 0 && outside.has(id);
280
+ });
281
+ const relationships = await context.pkg.relationships(record.slide.part);
282
+ const removedIds = [
283
+ record.element.id,
284
+ ...descendantIds(elements, record),
285
+ ];
286
+ const items = [patches.removeElement(part, record.node)];
287
+ const warnings = [];
288
+ // An animation that targets a removed shape makes PowerPoint repair
289
+ // the file; the slide's timing goes with the shape, and the host hears.
290
+ const timing = part.root.children.find((child) => child.local === "timing");
291
+ const shapeIds = new Set([
292
+ record,
293
+ ...elements.records.filter((candidate) => removedIds.includes(candidate.element.id)),
294
+ ].map((candidate) => String(candidate.cNvPrId)));
295
+ if (timing &&
296
+ part
297
+ .findAll("spTgt", timing)
298
+ .some((node) => shapeIds.has(part.attribute(node, "spid") ?? ""))) {
299
+ items.push(patches.removeElement(part, timing));
300
+ warnings.push({
301
+ code: "fidelity-degraded",
302
+ message: "The slide's animations were removed with the shape they targeted",
303
+ details: {
304
+ reason: "animations-removed",
305
+ elementId: record.element.id,
306
+ },
307
+ });
308
+ }
309
+ return commit(context, record, items, {
310
+ removedIds,
311
+ relationships: exclusive.filter((id) => relationships.byId(id)),
312
+ warnings,
313
+ });
314
+ },
315
+ };
316
+ /* Fill and line */
317
+ function fillXml(value) {
318
+ return value === "none" ? "<a:noFill/>" : solidFillXml(value);
319
+ }
320
+ /** The spPr content with the fill and line changes applied. */
321
+ function styledShapeProperties(part, spPr, operation) {
322
+ const children = [];
323
+ if (spPr)
324
+ for (const child of spPr.children)
325
+ children.push({
326
+ local: child.local,
327
+ xml: sliceOf(part, child),
328
+ node: child,
329
+ });
330
+ const replace = (matches, after, local, xml) => {
331
+ const first = children.findIndex((child) => matches(child.local));
332
+ for (let index = children.length - 1; index >= 0; index -= 1)
333
+ if (matches(children[index].local))
334
+ children.splice(index, 1);
335
+ if (xml === undefined)
336
+ return;
337
+ let at = first >= 0
338
+ ? first
339
+ : children.findIndex((child) => after.has(child.local));
340
+ if (at < 0)
341
+ at = children.length;
342
+ children.splice(at, 0, { local, xml });
343
+ };
344
+ if (operation.fill !== undefined)
345
+ replace((local) => FILLS.has(local), AFTER_SHAPE_FILL, "fill", operation.fill === null ? undefined : fillXml(operation.fill));
346
+ if (operation.line !== undefined) {
347
+ const existing = children.find((child) => child.local === "ln");
348
+ let xml;
349
+ if (operation.line === null)
350
+ xml = undefined;
351
+ else {
352
+ const color = operation.line === "none" ? "none" : operation.line.color;
353
+ const width = operation.line === "none" ? undefined : operation.line.width;
354
+ xml = lineXml(part, existing?.node, color, width);
355
+ }
356
+ replace((local) => local === "ln", AFTER_LINE, "ln", xml);
357
+ }
358
+ return children.map((child) => child.xml).join("");
359
+ }
360
+ /** `a:ln` with the fill replaced and the width set, other children and attributes kept. */
361
+ function lineXml(part, existing, color, width) {
362
+ const attributes = new Map();
363
+ const children = [];
364
+ if (existing) {
365
+ for (const attribute of existing.attributes)
366
+ attributes.set(attribute.name, attribute.rawValue);
367
+ for (const child of existing.children)
368
+ children.push({ local: child.local, xml: sliceOf(part, child) });
369
+ }
370
+ if (width !== undefined)
371
+ attributes.set("w", String(Math.round(width * 12700)));
372
+ const first = children.findIndex((child) => FILLS.has(child.local));
373
+ for (let index = children.length - 1; index >= 0; index -= 1)
374
+ if (FILLS.has(children[index].local))
375
+ children.splice(index, 1);
376
+ children.splice(first >= 0 ? first : 0, 0, {
377
+ local: "fill",
378
+ xml: fillXml(color),
379
+ });
380
+ const attributeText = [...attributes]
381
+ .map(([name, raw]) => ` ${name}="${raw}"`)
382
+ .join("");
383
+ return `<a:ln${attributeText}>${children.map((child) => child.xml).join("")}</a:ln>`;
384
+ }
385
+ export const setShapeStyleHandler = {
386
+ async validate(operation, context, issue) {
387
+ if (operation.fill !== undefined &&
388
+ operation.fill !== null &&
389
+ operation.fill !== "none") {
390
+ const problem = colorProblem(operation.fill);
391
+ if (problem)
392
+ issue("/fill", "invalid-value", `Colour: ${problem}`);
393
+ }
394
+ if (operation.line !== undefined &&
395
+ operation.line !== null &&
396
+ operation.line !== "none" &&
397
+ operation.line.color !== "none") {
398
+ const problem = colorProblem(operation.line.color);
399
+ if (problem)
400
+ issue("/line/color", "invalid-value", `Colour: ${problem}`);
401
+ }
402
+ const record = await editableTarget(operation.target, context, issue);
403
+ if (!record)
404
+ return;
405
+ const { kind } = record.element;
406
+ if (kind !== "shape" && kind !== "connector")
407
+ issue("/target", "invalid-target", `Element ${operation.target} has no shape style`);
408
+ else if (kind === "connector" && operation.fill !== undefined)
409
+ issue("/fill", "invalid-value", "A connector has no fill");
410
+ },
411
+ async apply(operation, context) {
412
+ const record = (await editableTarget(operation.target, context, () => { }));
413
+ const { part } = record;
414
+ const content = styledShapeProperties(part, record.spPr, operation);
415
+ if (record.spPr)
416
+ return commit(context, record, [
417
+ patches.replaceContent(part, record.spPr, content),
418
+ ]);
419
+ const txBody = record.node.children.find((child) => child.local === "txBody");
420
+ const spPr = `<p:spPr>${content}</p:spPr>`;
421
+ return commit(context, record, [
422
+ txBody
423
+ ? patches.insertBefore(part, txBody, spPr)
424
+ : patches.appendChild(part, record.node, spPr),
425
+ ]);
426
+ },
427
+ };
428
+ /* Text boxes */
429
+ /** Paragraph XML for a text and a style, as a new text box or table cell carries it. */
430
+ export function paragraphsXml(part, text, style) {
431
+ const { align, ...runChange } = style ?? {};
432
+ const rPr = Object.keys(runChange).length > 0
433
+ ? changedRunProperties(part, undefined, runChange)
434
+ : "";
435
+ const endParaRPr = rPr
436
+ ? rPr
437
+ .replace(/^<a:rPr/, "<a:endParaRPr")
438
+ .replace(/<\/a:rPr>$/, "</a:endParaRPr>")
439
+ : "";
440
+ const pPr = align ? `<a:pPr algn="${alignValue(align)}"/>` : "";
441
+ return text
442
+ .split(PARAGRAPH_BREAK)
443
+ .map((segment) => paragraphXml({ pPr, items: itemsOfSegment(segment, rPr), endParaRPr }))
444
+ .join("");
445
+ }
446
+ export const insertTextBoxHandler = {
447
+ async validate(operation, context, issue) {
448
+ if (operation.pageIndex < 0 || operation.pageIndex >= context.pageCount)
449
+ issue("/pageIndex", "unknown-target", `No slide ${operation.pageIndex}`);
450
+ const problem = textProblem(normalizeText(operation.text));
451
+ if (problem)
452
+ issue("/text", "invalid-text", `The text holds ${problem}`);
453
+ if (operation.style?.color !== undefined) {
454
+ const colour = colorProblem(operation.style.color);
455
+ if (colour)
456
+ issue("/style/color", "invalid-value", `Colour: ${colour}`);
457
+ }
458
+ if (operation.style?.fontFamily !== undefined) {
459
+ const font = textProblem(operation.style.fontFamily);
460
+ if (font)
461
+ issue("/style/fontFamily", "invalid-value", `Font: ${font}`);
462
+ }
463
+ },
464
+ async apply(operation, context) {
465
+ const elements = await context.elements(operation.pageIndex);
466
+ const { part, slide } = elements;
467
+ const tree = part.find("spTree");
468
+ if (!tree)
469
+ throw new Error(`Slide ${slide.part} has no shape tree`);
470
+ const id = context.allocateShapeId(elements);
471
+ const { rect } = operation;
472
+ const xml = `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="TextBox ${id - 1}"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>` +
473
+ `<p:spPr><a:xfrm><a:off x="${pxToEmu(rect.x)}" y="${pxToEmu(rect.y)}"/><a:ext cx="${pxToEmu(rect.width)}" cy="${pxToEmu(rect.height)}"/></a:xfrm>` +
474
+ `<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr>` +
475
+ `<p:txBody><a:bodyPr wrap="square" rtlCol="0"><a:spAutoFit/></a:bodyPr><a:lstStyle/>${paragraphsXml(part, normalizeText(operation.text), operation.style)}</p:txBody></p:sp>`;
476
+ const transaction = context.pkg.transaction();
477
+ transaction.patch(part, [patches.appendChild(part, tree, xml)]);
478
+ const change = await transaction.commit();
479
+ return {
480
+ createdIds: [`${slide.key}:${id}`],
481
+ changedPages: [operation.pageIndex],
482
+ warnings: change.warnings,
483
+ parts: committedParts(change),
484
+ };
485
+ },
486
+ };
@@ -0,0 +1,6 @@
1
+ import { type PptxOperationHandler } from "./operations.js";
2
+ import type { PptxDeleteSlideOperation, PptxDuplicateSlideOperation, PptxInsertSlideOperation, PptxMoveSlideOperation } from "./types.js";
3
+ export declare const insertSlideHandler: PptxOperationHandler<PptxInsertSlideOperation>;
4
+ export declare const duplicateSlideHandler: PptxOperationHandler<PptxDuplicateSlideOperation>;
5
+ export declare const deleteSlideHandler: PptxOperationHandler<PptxDeleteSlideOperation>;
6
+ export declare const moveSlideHandler: PptxOperationHandler<PptxMoveSlideOperation>;