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,458 @@
|
|
|
1
|
+
import { patches } from "../ooxml/patch.js";
|
|
2
|
+
import { W_NS } from "./ids.js";
|
|
3
|
+
import { changedParagraphProperties, changedRunProperties, attributeProblem, colorProblem, namespacePatches, normalizeText, paragraphMarkProperties, paragraphPropertiesWithoutSection, paragraphXml, runContentXml, runXml, sliceOf, textProblem, } from "./write.js";
|
|
4
|
+
/** The child of `container` an item sits under. */
|
|
5
|
+
function topOf(node, container) {
|
|
6
|
+
let current = node;
|
|
7
|
+
while (current.parent && current.parent !== container)
|
|
8
|
+
current = current.parent;
|
|
9
|
+
return current;
|
|
10
|
+
}
|
|
11
|
+
/** The units of a container's children for the items it holds. */
|
|
12
|
+
function unitsOf(container, items) {
|
|
13
|
+
const byTop = new Map();
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
const top = topOf(item.run, container);
|
|
16
|
+
const list = byTop.get(top) ?? [];
|
|
17
|
+
list.push(item);
|
|
18
|
+
byTop.set(top, list);
|
|
19
|
+
}
|
|
20
|
+
// A complex field spans consecutive children: from its begin run's top
|
|
21
|
+
// to its end run's top.
|
|
22
|
+
const groupEnd = new Map();
|
|
23
|
+
for (const item of items)
|
|
24
|
+
if (item.fieldRuns && item.fieldRuns.length > 1) {
|
|
25
|
+
const first = topOf(item.fieldRuns[0], container);
|
|
26
|
+
const last = topOf(item.fieldRuns.at(-1), container);
|
|
27
|
+
if (first !== last)
|
|
28
|
+
groupEnd.set(first, last);
|
|
29
|
+
}
|
|
30
|
+
const units = [];
|
|
31
|
+
let position = 0;
|
|
32
|
+
const children = container.children.filter((child) => !(child.local === "pPr" && child.namespace === W_NS));
|
|
33
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
34
|
+
const child = children[index];
|
|
35
|
+
const end = groupEnd.get(child);
|
|
36
|
+
const nodes = [child];
|
|
37
|
+
if (end) {
|
|
38
|
+
let cursor = index + 1;
|
|
39
|
+
while (cursor < children.length && children[cursor - 1] !== end)
|
|
40
|
+
nodes.push(children[cursor++]);
|
|
41
|
+
index = cursor - 1;
|
|
42
|
+
}
|
|
43
|
+
const own = nodes.flatMap((node) => byTop.get(node) ?? []);
|
|
44
|
+
const kind = own.length === 0
|
|
45
|
+
? "zero"
|
|
46
|
+
: end ||
|
|
47
|
+
child.local === "fldSimple" ||
|
|
48
|
+
own.some((item) => item.fieldRuns)
|
|
49
|
+
? "field"
|
|
50
|
+
: child.local === "r"
|
|
51
|
+
? "run"
|
|
52
|
+
: "wrapper";
|
|
53
|
+
const start = own.length > 0 ? own[0].start : position;
|
|
54
|
+
const stop = own.length > 0 ? own.at(-1).end : position;
|
|
55
|
+
units.push({ nodes, kind, start, end: stop, items: own });
|
|
56
|
+
position = stop;
|
|
57
|
+
}
|
|
58
|
+
return units;
|
|
59
|
+
}
|
|
60
|
+
function rPrOf(part, item) {
|
|
61
|
+
return item ? sliceOf(part, item.rPr) : "";
|
|
62
|
+
}
|
|
63
|
+
function startTagOf(part, node) {
|
|
64
|
+
return part.text.slice(node.start, node.contentStart);
|
|
65
|
+
}
|
|
66
|
+
function endTagOf(part, node) {
|
|
67
|
+
return part.text.slice(node.contentEnd, node.end);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The content of a run's items in `[from, to]`: `w:t` text sliced, every
|
|
71
|
+
* other child (tab, break, picture, symbol, note reference) as its bytes
|
|
72
|
+
* when it lies inside. A zero-width item at `from` belongs to the slice
|
|
73
|
+
* only when `includeStart` says so, which is how a caret or a range edge
|
|
74
|
+
* hands such an item to exactly one side.
|
|
75
|
+
*/
|
|
76
|
+
function contentOf(part, items, from, to, includeStart) {
|
|
77
|
+
let out = "";
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
if (item.start === item.end) {
|
|
80
|
+
const after = includeStart ? item.start >= from : item.start > from;
|
|
81
|
+
if (after && item.start <= to)
|
|
82
|
+
out += sliceOf(part, item.child);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const begin = Math.max(item.start, from);
|
|
86
|
+
const stop = Math.min(item.end, to);
|
|
87
|
+
if (stop <= begin)
|
|
88
|
+
continue;
|
|
89
|
+
if (item.kind === "text" && item.child.local === "t")
|
|
90
|
+
out += runContentXml(item.text.slice(begin - item.start, stop - item.start));
|
|
91
|
+
else if (begin === item.start && stop === item.end)
|
|
92
|
+
out += sliceOf(part, item.child);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Inline objects a replacement of `[from, to)` drops: pictures, objects
|
|
98
|
+
* and anchors inside the range (a zero-width one only strictly inside),
|
|
99
|
+
* by element id, with the ids of every paragraph nested in them (a text
|
|
100
|
+
* box) so the engine's id list follows.
|
|
101
|
+
*/
|
|
102
|
+
function droppedInlines(model, record, items, from, to) {
|
|
103
|
+
const removedIds = [];
|
|
104
|
+
const removedParagraphIds = [];
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
if (item.kind !== "picture" &&
|
|
107
|
+
item.kind !== "object" &&
|
|
108
|
+
item.kind !== "anchor")
|
|
109
|
+
continue;
|
|
110
|
+
const inside = item.start === item.end
|
|
111
|
+
? from < item.start && item.start < to
|
|
112
|
+
: item.start >= from && item.end <= to;
|
|
113
|
+
if (!inside)
|
|
114
|
+
continue;
|
|
115
|
+
const inline = record.inlines.find((candidate) => candidate.node === item.child);
|
|
116
|
+
if (inline)
|
|
117
|
+
removedIds.push(inline.elementId);
|
|
118
|
+
for (const paragraph of model.document.findAll("p", item.child)) {
|
|
119
|
+
const id = model.paragraphIds.get(paragraph);
|
|
120
|
+
if (id && paragraph.namespace === W_NS)
|
|
121
|
+
removedParagraphIds.push(id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { removedIds, removedParagraphIds };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The container's content with `[start, end)` removed and `insert(rPr)`
|
|
128
|
+
* written at `start`: `before` holds everything up to and including the
|
|
129
|
+
* inserted text, `after` what follows. New text is styled like the run
|
|
130
|
+
* that held the first replaced character, like the run before a caret,
|
|
131
|
+
* else like the run after it.
|
|
132
|
+
*/
|
|
133
|
+
function splitAt(part, container, items, start, end, insert, fallbackRPr) {
|
|
134
|
+
let before = "";
|
|
135
|
+
let after = "";
|
|
136
|
+
let inserted = false;
|
|
137
|
+
let nested = false;
|
|
138
|
+
let rPr = fallbackRPr;
|
|
139
|
+
let lastRPr;
|
|
140
|
+
const raw = (unit) => unit.nodes.map((node) => sliceOf(part, node)).join("");
|
|
141
|
+
const place = (style) => {
|
|
142
|
+
rPr = style ?? lastRPr ?? fallbackRPr;
|
|
143
|
+
before += insert(rPr);
|
|
144
|
+
inserted = true;
|
|
145
|
+
};
|
|
146
|
+
for (const unit of unitsOf(container, items)) {
|
|
147
|
+
if (unit.kind === "zero") {
|
|
148
|
+
if (inserted)
|
|
149
|
+
after += raw(unit);
|
|
150
|
+
else
|
|
151
|
+
before += raw(unit);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (unit.end <= start) {
|
|
155
|
+
before += raw(unit);
|
|
156
|
+
lastRPr = rPrOf(part, unit.items.at(-1));
|
|
157
|
+
if (!inserted && start === end && unit.end === start)
|
|
158
|
+
place(lastRPr);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (unit.start >= end) {
|
|
162
|
+
if (!inserted)
|
|
163
|
+
place(lastRPr ?? rPrOf(part, unit.items[0]));
|
|
164
|
+
after += raw(unit);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (unit.start >= start && unit.end <= end) {
|
|
168
|
+
// Covered whole: dropped; the first such unit styles the new text.
|
|
169
|
+
if (!inserted)
|
|
170
|
+
place(rPrOf(part, unit.items[0]));
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (unit.kind === "run") {
|
|
174
|
+
const style = rPrOf(part, unit.items[0]);
|
|
175
|
+
// The head keeps a zero-width item at `start`; the tail takes one at
|
|
176
|
+
// `end` only when the range is not a caret (the head has it then).
|
|
177
|
+
const head = runXml(style, contentOf(part, unit.items, unit.start, start, true));
|
|
178
|
+
const tail = runXml(style, contentOf(part, unit.items, end, unit.end, start < end));
|
|
179
|
+
if (!inserted) {
|
|
180
|
+
before += head;
|
|
181
|
+
place(style);
|
|
182
|
+
after += tail;
|
|
183
|
+
}
|
|
184
|
+
else
|
|
185
|
+
after += tail;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (unit.kind === "wrapper" && unit.start < start && end < unit.end) {
|
|
189
|
+
// Both ends inside: the wrapper stays and its runs change.
|
|
190
|
+
const wrapper = unit.nodes[0];
|
|
191
|
+
const inner = splitAt(part, wrapper, unit.items, start, end, insert, fallbackRPr);
|
|
192
|
+
before +=
|
|
193
|
+
startTagOf(part, wrapper) +
|
|
194
|
+
inner.before +
|
|
195
|
+
inner.after +
|
|
196
|
+
endTagOf(part, wrapper);
|
|
197
|
+
rPr = inner.rPr;
|
|
198
|
+
inserted = true;
|
|
199
|
+
nested = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// A field cut, a wrapper edge cut: validation refuses these.
|
|
203
|
+
throw new Error("A range cannot cut a field or a wrapper");
|
|
204
|
+
}
|
|
205
|
+
if (!inserted)
|
|
206
|
+
place(lastRPr);
|
|
207
|
+
return { before, after, rPr, nested };
|
|
208
|
+
}
|
|
209
|
+
async function textTarget(id, context, issue) {
|
|
210
|
+
const record = context.model.byId.get(id);
|
|
211
|
+
if (!record || record.kind !== "paragraph") {
|
|
212
|
+
issue("/target", record ? "invalid-target" : "unknown-target", record ? `Element ${id} is not a paragraph` : `No element ${id}`);
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
if (record.readOnlyReason) {
|
|
216
|
+
issue("/target", "invalid-target", `Paragraph ${id} is read-only (${record.readOnlyReason})`);
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
return { record, part: context.model.document };
|
|
220
|
+
}
|
|
221
|
+
/** Checks a range against its target; returns the clamped offsets. */
|
|
222
|
+
function checkRange(target, id, range, issue, structural) {
|
|
223
|
+
const text = target.record.text.text;
|
|
224
|
+
const length = text.length;
|
|
225
|
+
if (!range)
|
|
226
|
+
return { start: 0, end: length };
|
|
227
|
+
if (range.start.elementId !== id || range.end.elementId !== id) {
|
|
228
|
+
issue("/range", "invalid-range", "Both ends must lie on the target");
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const { start, end } = { start: range.start.offset, end: range.end.offset };
|
|
232
|
+
if (start > length || end > length || start > end) {
|
|
233
|
+
issue("/range", "invalid-range", `Offsets must satisfy 0 ≤ start ≤ end ≤ ${length}`);
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
for (const offset of [start, end])
|
|
237
|
+
if (offset > 0 &&
|
|
238
|
+
offset < length &&
|
|
239
|
+
(text.charCodeAt(offset) & 0xfc00) === 0xdc00 &&
|
|
240
|
+
(text.charCodeAt(offset - 1) & 0xfc00) === 0xd800) {
|
|
241
|
+
issue("/range", "invalid-range", "A range cannot split a surrogate pair");
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
if (!structural)
|
|
245
|
+
return { start, end };
|
|
246
|
+
const inside = (unit, offset) => unit.start < offset && offset < unit.end;
|
|
247
|
+
const check = (container, items) => {
|
|
248
|
+
for (const unit of unitsOf(container, items)) {
|
|
249
|
+
if (unit.kind === "field" && (inside(unit, start) || inside(unit, end))) {
|
|
250
|
+
issue("/range", "invalid-range", "A range cannot start or end inside a field");
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (unit.kind !== "wrapper")
|
|
254
|
+
continue;
|
|
255
|
+
const startIn = inside(unit, start);
|
|
256
|
+
const endIn = inside(unit, end);
|
|
257
|
+
if (startIn !== endIn) {
|
|
258
|
+
issue("/range", "invalid-range", "A range cannot cut the edge of a hyperlink or content control");
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (startIn && !check(unit.nodes[0], unit.items))
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
};
|
|
266
|
+
return check(target.record.node, target.record.text.items)
|
|
267
|
+
? { start, end }
|
|
268
|
+
: undefined;
|
|
269
|
+
}
|
|
270
|
+
function commitParagraph(context, target, items, extra = {}) {
|
|
271
|
+
const { part, record } = target;
|
|
272
|
+
const transaction = context.pkg.transaction();
|
|
273
|
+
transaction.patch(part, [...namespacePatches(part), ...items]);
|
|
274
|
+
return transaction.commit().then((change) => ({
|
|
275
|
+
createdIds: [],
|
|
276
|
+
warnings: change.warnings,
|
|
277
|
+
...(context.model.unauthoredSet.has(record.id)
|
|
278
|
+
? { stamped: [record.id] }
|
|
279
|
+
: {}),
|
|
280
|
+
reflowFrom: record.id,
|
|
281
|
+
...extra,
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
export const replaceTextHandler = {
|
|
285
|
+
async validate(operation, context, issue) {
|
|
286
|
+
const text = normalizeText(operation.text);
|
|
287
|
+
const problem = textProblem(text);
|
|
288
|
+
if (problem)
|
|
289
|
+
issue("/text", "invalid-text", `The text holds ${problem}`);
|
|
290
|
+
const target = await textTarget(operation.target, context, issue);
|
|
291
|
+
if (!target)
|
|
292
|
+
return;
|
|
293
|
+
const range = checkRange(target, operation.target, operation.range, issue, true);
|
|
294
|
+
if (!range || !text.includes("\n"))
|
|
295
|
+
return;
|
|
296
|
+
// A paragraph cannot be split inside a hyperlink or content control.
|
|
297
|
+
const probe = splitAt(target.part, target.record.node, target.record.text.items, range.start, range.end, () => "", "");
|
|
298
|
+
if (probe.nested)
|
|
299
|
+
issue("/text", "invalid-range", "A paragraph break cannot be inserted inside a hyperlink or content control");
|
|
300
|
+
},
|
|
301
|
+
async apply(operation, context) {
|
|
302
|
+
const target = (await textTarget(operation.target, context, () => { }));
|
|
303
|
+
const { start, end } = checkRange(target, operation.target, operation.range, () => { }, true);
|
|
304
|
+
const { items, createdIds, removedIds, removedParagraphIds } = replacedParagraph(context, target.record, start, end, normalizeText(operation.text));
|
|
305
|
+
return commitParagraph(context, target, items, {
|
|
306
|
+
createdIds,
|
|
307
|
+
...(removedIds.length > 0 ? { removedIds } : {}),
|
|
308
|
+
...(removedParagraphIds.length > 0 ? { removedParagraphIds } : {}),
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* The patches that replace `[start, end)` of a paragraph's text with
|
|
314
|
+
* `text`, newlines splitting it into new paragraphs after it: the first
|
|
315
|
+
* paragraph rebuilt in place, the others inserted with fresh ids.
|
|
316
|
+
*/
|
|
317
|
+
export function replacedParagraph(context, record, start, end, text) {
|
|
318
|
+
const part = context.model.document;
|
|
319
|
+
const segments = text.split("\n");
|
|
320
|
+
const split = splitAt(part, record.node, record.text.items, start, end, (rPr) => runXml(rPr, runContentXml(segments[0])), paragraphMarkRPr(part, record));
|
|
321
|
+
const pPr = sliceOf(part, record.pPr);
|
|
322
|
+
const single = segments.length === 1;
|
|
323
|
+
const first = paragraphXml(record.node, record.id, pPr, split.before + (single ? split.after : ""));
|
|
324
|
+
const items = [patches.replaceElement(part, record.node, first)];
|
|
325
|
+
const createdIds = [];
|
|
326
|
+
const copiedPPr = paragraphPropertiesWithoutSection(part, record.pPr);
|
|
327
|
+
segments.slice(1).forEach((segment, index) => {
|
|
328
|
+
const id = context.freshParagraphId();
|
|
329
|
+
createdIds.push(`p:${id}`);
|
|
330
|
+
const last = index === segments.length - 2;
|
|
331
|
+
items.push(patches.insertAfter(part, record.node, paragraphXml(record.node, id, copiedPPr, runXml(split.rPr, runContentXml(segment)) + (last ? split.after : ""))));
|
|
332
|
+
});
|
|
333
|
+
const dropped = droppedInlines(context.model, record, record.text.items, start, end);
|
|
334
|
+
const removedIds = dropped.removedIds;
|
|
335
|
+
// Inline objects that moved to a new paragraph change their id.
|
|
336
|
+
if (!single)
|
|
337
|
+
for (const inline of record.inlines) {
|
|
338
|
+
const item = record.text.items.find((candidate) => candidate.child === inline.node);
|
|
339
|
+
if (item && item.start >= end && !removedIds.includes(inline.elementId))
|
|
340
|
+
removedIds.push(inline.elementId);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
items,
|
|
344
|
+
createdIds,
|
|
345
|
+
removedIds,
|
|
346
|
+
removedParagraphIds: dropped.removedParagraphIds,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/** The paragraph mark's `w:rPr`, the style of text in an empty paragraph. */
|
|
350
|
+
function paragraphMarkRPr(part, record) {
|
|
351
|
+
const rPr = record.pPr?.children.find((child) => child.local === "rPr" && child.namespace === W_NS);
|
|
352
|
+
return sliceOf(part, rPr);
|
|
353
|
+
}
|
|
354
|
+
/** A run re-serialized with new properties around its original content. */
|
|
355
|
+
function restyledRun(part, run, rPr) {
|
|
356
|
+
const content = run.children
|
|
357
|
+
.filter((child) => !(child.local === "rPr" && child.namespace === W_NS))
|
|
358
|
+
.map((child) => sliceOf(part, child))
|
|
359
|
+
.join("");
|
|
360
|
+
return `<w:r>${rPr}${content}</w:r>`;
|
|
361
|
+
}
|
|
362
|
+
/** The container's content with the runs in `[start, end)` restyled. */
|
|
363
|
+
function restyled(part, container, items, start, end, change, context) {
|
|
364
|
+
let out = "";
|
|
365
|
+
const changed = (rPr) => changedRunProperties(part, rPr, change, context.model.styles);
|
|
366
|
+
for (const unit of unitsOf(container, items)) {
|
|
367
|
+
const raw = unit.nodes.map((node) => sliceOf(part, node)).join("");
|
|
368
|
+
const overlaps = unit.start < end && start < unit.end
|
|
369
|
+
? true
|
|
370
|
+
: unit.start === unit.end && start < unit.start && unit.start < end;
|
|
371
|
+
if (unit.kind === "zero" || !overlaps) {
|
|
372
|
+
out += raw;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (unit.kind === "run") {
|
|
376
|
+
const run = unit.nodes[0];
|
|
377
|
+
const own = rPrOf(part, unit.items[0]);
|
|
378
|
+
const rPr = unit.items[0]?.rPr;
|
|
379
|
+
out += runXml(own, contentOf(part, unit.items, unit.start, Math.min(start, unit.end), true));
|
|
380
|
+
out += runXml(changed(rPr), contentOf(part, unit.items, Math.max(unit.start, start), Math.min(unit.end, end), false));
|
|
381
|
+
out += runXml(own, contentOf(part, unit.items, Math.max(end, unit.start), unit.end, false));
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (unit.kind === "field") {
|
|
385
|
+
// A field is styled whole: every run of it takes the change.
|
|
386
|
+
for (const node of unit.nodes)
|
|
387
|
+
if (node.local === "r" && node.namespace === W_NS)
|
|
388
|
+
out += restyledRun(part, node, changed(node.children.find((child) => child.local === "rPr" && child.namespace === W_NS)));
|
|
389
|
+
else if (node.local === "fldSimple")
|
|
390
|
+
out +=
|
|
391
|
+
startTagOf(part, node) +
|
|
392
|
+
restyled(part, node, unit.items, start, end, change, context) +
|
|
393
|
+
endTagOf(part, node);
|
|
394
|
+
else
|
|
395
|
+
out += sliceOf(part, node);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const wrapper = unit.nodes[0];
|
|
399
|
+
out +=
|
|
400
|
+
startTagOf(part, wrapper) +
|
|
401
|
+
restyled(part, wrapper, unit.items, start, end, change, context) +
|
|
402
|
+
endTagOf(part, wrapper);
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
export const setTextStyleHandler = {
|
|
407
|
+
async validate(operation, context, issue) {
|
|
408
|
+
const { style } = operation;
|
|
409
|
+
if (style.color !== undefined) {
|
|
410
|
+
const problem = colorProblem(style.color);
|
|
411
|
+
if (problem)
|
|
412
|
+
issue("/style/color", "invalid-value", `Colour: ${problem}`);
|
|
413
|
+
}
|
|
414
|
+
if (style.fontFamily !== undefined) {
|
|
415
|
+
const problem = attributeProblem(style.fontFamily);
|
|
416
|
+
if (problem)
|
|
417
|
+
issue("/style/fontFamily", "invalid-value", `Font: ${problem}`);
|
|
418
|
+
}
|
|
419
|
+
const target = await textTarget(operation.target, context, issue);
|
|
420
|
+
if (target)
|
|
421
|
+
checkRange(target, operation.target, operation.range, issue, false);
|
|
422
|
+
},
|
|
423
|
+
async apply(operation, context) {
|
|
424
|
+
const target = (await textTarget(operation.target, context, () => { }));
|
|
425
|
+
const { part, record } = target;
|
|
426
|
+
const { start, end } = checkRange(target, operation.target, operation.range, () => { }, false);
|
|
427
|
+
if (Object.keys(operation.style).length === 0)
|
|
428
|
+
return { createdIds: [], warnings: [] };
|
|
429
|
+
const content = restyled(part, record.node, record.text.items, start, end, operation.style, context);
|
|
430
|
+
const pPr = end >= record.text.text.length
|
|
431
|
+
? paragraphMarkProperties(part, record.pPr, operation.style, context.model.styles)
|
|
432
|
+
: sliceOf(part, record.pPr);
|
|
433
|
+
return commitParagraph(context, target, [
|
|
434
|
+
patches.replaceElement(part, record.node, paragraphXml(record.node, record.id, pPr, content)),
|
|
435
|
+
]);
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
export const setParagraphStyleHandler = {
|
|
439
|
+
async validate(operation, context, issue) {
|
|
440
|
+
await textTarget(operation.target, context, issue);
|
|
441
|
+
},
|
|
442
|
+
async apply(operation, context) {
|
|
443
|
+
const target = (await textTarget(operation.target, context, () => { }));
|
|
444
|
+
const { part, record } = target;
|
|
445
|
+
const { style } = operation;
|
|
446
|
+
if (style.align === undefined &&
|
|
447
|
+
(style.spacing === undefined || Object.keys(style.spacing).length === 0))
|
|
448
|
+
return { createdIds: [], warnings: [] };
|
|
449
|
+
const pPr = changedParagraphProperties(part, record.pPr, style);
|
|
450
|
+
const node = record.node;
|
|
451
|
+
const content = node.selfClosing
|
|
452
|
+
? ""
|
|
453
|
+
: part.text.slice(record.pPr ? record.pPr.end : node.contentStart, node.contentEnd);
|
|
454
|
+
return commitParagraph(context, target, [
|
|
455
|
+
patches.replaceElement(part, node, paragraphXml(node, record.id, pPr, content)),
|
|
456
|
+
]);
|
|
457
|
+
},
|
|
458
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { XmlElement, XmlPart } from "../ooxml/xml.js";
|
|
2
|
+
export declare const TAB = "\t";
|
|
3
|
+
export declare const LINE_BREAK = "\v";
|
|
4
|
+
export declare const PAGE_BREAK = "\f";
|
|
5
|
+
/** The object replacement character: an inline picture or an embedded object. */
|
|
6
|
+
export declare const OBJECT = "\uFFFC";
|
|
7
|
+
export type RunItemKind = "text" | "tab" | "break" | "pageBreak" | "picture" | "anchor" | "object" | "field" | "note";
|
|
8
|
+
export interface RunItem {
|
|
9
|
+
readonly kind: RunItemKind;
|
|
10
|
+
/** Half-open offsets in the paragraph's text. */
|
|
11
|
+
readonly start: number;
|
|
12
|
+
readonly end: number;
|
|
13
|
+
readonly text: string;
|
|
14
|
+
/** The `w:r` holding the item, or the `w:fldSimple` of a simple field. */
|
|
15
|
+
readonly run: XmlElement;
|
|
16
|
+
/** The child of the run the item comes from (`w:t`, `w:tab`, `w:drawing`, …). */
|
|
17
|
+
readonly child: XmlElement;
|
|
18
|
+
/** `w:rPr` of the run, when present. */
|
|
19
|
+
readonly rPr?: XmlElement;
|
|
20
|
+
/** The hyperlink, inline sdt, smart tag or tracked-change wrapper the run sits in, when any. */
|
|
21
|
+
readonly wrapper?: XmlElement;
|
|
22
|
+
/** For a complex field: every run from `begin` to `end`, in order. */
|
|
23
|
+
readonly fieldRuns?: readonly XmlElement[];
|
|
24
|
+
}
|
|
25
|
+
export interface ParagraphText {
|
|
26
|
+
readonly text: string;
|
|
27
|
+
readonly items: readonly RunItem[];
|
|
28
|
+
/** The paragraph holds tracked insertions, deletions or moves. */
|
|
29
|
+
readonly tracked: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Reads the text of a `w:p`. */
|
|
32
|
+
export declare function readParagraphText(part: XmlPart, paragraph: XmlElement): ParagraphText;
|
|
33
|
+
/** The first item that draws text, for the resolved style of a paragraph. */
|
|
34
|
+
export declare function firstTextItem(model: ParagraphText): RunItem | undefined;
|