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