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,375 @@
|
|
|
1
|
+
import { OFFICE_RELATIONSHIPS, W_NS } from "./ids.js";
|
|
2
|
+
/*
|
|
3
|
+
* Resolved styles of WordprocessingML. A run property comes from the first
|
|
4
|
+
* of: the run's own `w:rPr`, its character style chain (`w:rStyle`, then
|
|
5
|
+
* `w:basedOn` up), the paragraph style chain's run properties, the default
|
|
6
|
+
* paragraph style, the document defaults, Word's built-in defaults. A
|
|
7
|
+
* paragraph property comes from `w:pPr`, the paragraph style chain, the
|
|
8
|
+
* default paragraph style, the document defaults. Theme fonts and colours
|
|
9
|
+
* are reported in their theme form where the file uses them.
|
|
10
|
+
*/
|
|
11
|
+
const HIGHLIGHTS = new Set([
|
|
12
|
+
"yellow",
|
|
13
|
+
"green",
|
|
14
|
+
"cyan",
|
|
15
|
+
"magenta",
|
|
16
|
+
"blue",
|
|
17
|
+
"red",
|
|
18
|
+
"darkBlue",
|
|
19
|
+
"darkCyan",
|
|
20
|
+
"darkGreen",
|
|
21
|
+
"darkMagenta",
|
|
22
|
+
"darkRed",
|
|
23
|
+
"darkYellow",
|
|
24
|
+
"darkGray",
|
|
25
|
+
"lightGray",
|
|
26
|
+
"black",
|
|
27
|
+
"white",
|
|
28
|
+
]);
|
|
29
|
+
const BUILTIN_FONTS = { major: "Calibri Light", minor: "Calibri" };
|
|
30
|
+
/** Word's `w:themeColor` names to the theme's colour scheme slots. */
|
|
31
|
+
const THEME_SLOTS = {
|
|
32
|
+
dark1: "dk1",
|
|
33
|
+
text1: "dk1",
|
|
34
|
+
light1: "lt1",
|
|
35
|
+
background1: "lt1",
|
|
36
|
+
dark2: "dk2",
|
|
37
|
+
text2: "dk2",
|
|
38
|
+
light2: "lt2",
|
|
39
|
+
background2: "lt2",
|
|
40
|
+
accent1: "accent1",
|
|
41
|
+
accent2: "accent2",
|
|
42
|
+
accent3: "accent3",
|
|
43
|
+
accent4: "accent4",
|
|
44
|
+
accent5: "accent5",
|
|
45
|
+
accent6: "accent6",
|
|
46
|
+
hyperlink: "hlink",
|
|
47
|
+
followedHyperlink: "folHlink",
|
|
48
|
+
};
|
|
49
|
+
/** Whether a name is one Word accepts in `w:themeColor`. */
|
|
50
|
+
export function isThemeColorName(name) {
|
|
51
|
+
return Object.hasOwn(THEME_SLOTS, name);
|
|
52
|
+
}
|
|
53
|
+
/** The styles and theme of a document, read once per revision. */
|
|
54
|
+
export class DocxStyles {
|
|
55
|
+
part;
|
|
56
|
+
styles;
|
|
57
|
+
defaultParagraph;
|
|
58
|
+
defaultCharacter;
|
|
59
|
+
docDefaultsRPr;
|
|
60
|
+
docDefaultsPPr;
|
|
61
|
+
fonts;
|
|
62
|
+
colors;
|
|
63
|
+
constructor(part, styles, defaultParagraph, defaultCharacter, docDefaultsRPr, docDefaultsPPr, fonts, colors) {
|
|
64
|
+
this.part = part;
|
|
65
|
+
this.styles = styles;
|
|
66
|
+
this.defaultParagraph = defaultParagraph;
|
|
67
|
+
this.defaultCharacter = defaultCharacter;
|
|
68
|
+
this.docDefaultsRPr = docDefaultsRPr;
|
|
69
|
+
this.docDefaultsPPr = docDefaultsPPr;
|
|
70
|
+
this.fonts = fonts;
|
|
71
|
+
this.colors = colors;
|
|
72
|
+
}
|
|
73
|
+
/** The `#RRGGBB` a `w:themeColor` name resolves to in this theme, when it has one. */
|
|
74
|
+
themeColor(name) {
|
|
75
|
+
const slot = THEME_SLOTS[name];
|
|
76
|
+
return slot ? this.colors.get(slot) : undefined;
|
|
77
|
+
}
|
|
78
|
+
static async load(pkg, documentPart, signal) {
|
|
79
|
+
const rels = await pkg.relationships(documentPart, signal);
|
|
80
|
+
const stylesPart = rels.byType(`${OFFICE_RELATIONSHIPS}styles`)[0]
|
|
81
|
+
?.targetPart;
|
|
82
|
+
const themePart = rels.byType(`${OFFICE_RELATIONSHIPS}theme`)[0]
|
|
83
|
+
?.targetPart;
|
|
84
|
+
const { fonts, colors } = await readTheme(pkg, themePart, signal);
|
|
85
|
+
if (!stylesPart || !pkg.has(stylesPart))
|
|
86
|
+
return new DocxStyles(undefined, new Map(), undefined, undefined, undefined, undefined, fonts, colors);
|
|
87
|
+
const part = await pkg.xml(stylesPart, signal);
|
|
88
|
+
const styles = new Map();
|
|
89
|
+
let defaultParagraph;
|
|
90
|
+
let defaultCharacter;
|
|
91
|
+
for (const node of part.root.children) {
|
|
92
|
+
if (node.namespace !== W_NS || node.local !== "style")
|
|
93
|
+
continue;
|
|
94
|
+
const id = part.attribute(node, "w:styleId");
|
|
95
|
+
if (!id)
|
|
96
|
+
continue;
|
|
97
|
+
const basedOn = part.attribute(node.children.find((child) => child.local === "basedOn") ?? node, "w:val");
|
|
98
|
+
const record = {
|
|
99
|
+
id,
|
|
100
|
+
type: part.attribute(node, "w:type") ?? "paragraph",
|
|
101
|
+
...(basedOn && basedOn !== id ? { basedOn } : {}),
|
|
102
|
+
isDefault: isOn(part.attribute(node, "w:default")),
|
|
103
|
+
...pick(node, "rPr", "pPr"),
|
|
104
|
+
};
|
|
105
|
+
styles.set(id, record);
|
|
106
|
+
if (record.isDefault && record.type === "paragraph" && !defaultParagraph)
|
|
107
|
+
defaultParagraph = record;
|
|
108
|
+
if (record.isDefault && record.type === "character" && !defaultCharacter)
|
|
109
|
+
defaultCharacter = record;
|
|
110
|
+
}
|
|
111
|
+
const docDefaults = part.root.children.find((child) => child.local === "docDefaults");
|
|
112
|
+
const rPrDefault = docDefaults?.children.find((child) => child.local === "rPrDefault");
|
|
113
|
+
const pPrDefault = docDefaults?.children.find((child) => child.local === "pPrDefault");
|
|
114
|
+
return new DocxStyles(part, styles, defaultParagraph, defaultCharacter, rPrDefault?.children.find((child) => child.local === "rPr"), pPrDefault?.children.find((child) => child.local === "pPr"), fonts, colors);
|
|
115
|
+
}
|
|
116
|
+
/** A style and the styles it is based on, nearest first; cycles stop. */
|
|
117
|
+
chain(id) {
|
|
118
|
+
const out = [];
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
let current = id;
|
|
121
|
+
while (current && !seen.has(current)) {
|
|
122
|
+
seen.add(current);
|
|
123
|
+
const record = this.styles.get(current);
|
|
124
|
+
if (!record)
|
|
125
|
+
break;
|
|
126
|
+
out.push(record);
|
|
127
|
+
current = record.basedOn;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function pick(node, ...names) {
|
|
133
|
+
const out = {};
|
|
134
|
+
for (const name of names) {
|
|
135
|
+
const child = node.children.find((candidate) => candidate.local === name && candidate.namespace === W_NS);
|
|
136
|
+
if (child)
|
|
137
|
+
out[name] = child;
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
async function readTheme(pkg, themePart, signal) {
|
|
142
|
+
const none = { fonts: BUILTIN_FONTS, colors: new Map() };
|
|
143
|
+
if (!themePart || !pkg.has(themePart))
|
|
144
|
+
return none;
|
|
145
|
+
try {
|
|
146
|
+
const xml = await pkg.xml(themePart, signal);
|
|
147
|
+
const face = (scheme) => {
|
|
148
|
+
const node = xml.find(scheme);
|
|
149
|
+
const latin = node?.children.find((child) => child.local === "latin");
|
|
150
|
+
return latin ? xml.attribute(latin, "typeface") : undefined;
|
|
151
|
+
};
|
|
152
|
+
const colors = new Map();
|
|
153
|
+
const scheme = xml.find("clrScheme");
|
|
154
|
+
for (const slot of scheme?.children ?? []) {
|
|
155
|
+
const value = slot.children[0];
|
|
156
|
+
const rgb = value
|
|
157
|
+
? value.local === "srgbClr"
|
|
158
|
+
? xml.attribute(value, "val")
|
|
159
|
+
: value.local === "sysClr"
|
|
160
|
+
? xml.attribute(value, "lastClr")
|
|
161
|
+
: undefined
|
|
162
|
+
: undefined;
|
|
163
|
+
if (rgb && /^[0-9A-Fa-f]{6}$/.test(rgb))
|
|
164
|
+
colors.set(slot.local, rgb.toUpperCase());
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
fonts: {
|
|
168
|
+
major: face("majorFont") || BUILTIN_FONTS.major,
|
|
169
|
+
minor: face("minorFont") || BUILTIN_FONTS.minor,
|
|
170
|
+
},
|
|
171
|
+
colors,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return none;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** `w:b`, `w:i`, `w:default`-style toggles: present without a value means on. */
|
|
179
|
+
export function isOn(value) {
|
|
180
|
+
return value === undefined || !/^(0|false|off)$/i.test(value);
|
|
181
|
+
}
|
|
182
|
+
/** The run property holders in resolution order for a run of a paragraph. */
|
|
183
|
+
export function runPropertySources(styles, pPr, rPr) {
|
|
184
|
+
const out = [];
|
|
185
|
+
if (rPr)
|
|
186
|
+
out.push(rPr);
|
|
187
|
+
const rStyle = rPr
|
|
188
|
+
? attributeOf(rPr.children.find((child) => child.local === "rStyle"), "w:val")
|
|
189
|
+
: undefined;
|
|
190
|
+
for (const record of styles.chain(rStyle))
|
|
191
|
+
if (record.rPr)
|
|
192
|
+
out.push(record.rPr);
|
|
193
|
+
const pStyle = pPr
|
|
194
|
+
? attributeOf(pPr.children.find((child) => child.local === "pStyle"), "w:val")
|
|
195
|
+
: undefined;
|
|
196
|
+
for (const record of styles.chain(pStyle ?? styles.defaultParagraph?.id))
|
|
197
|
+
if (record.rPr)
|
|
198
|
+
out.push(record.rPr);
|
|
199
|
+
if (pStyle &&
|
|
200
|
+
styles.defaultParagraph &&
|
|
201
|
+
!styles.chain(pStyle).includes(styles.defaultParagraph) &&
|
|
202
|
+
styles.defaultParagraph.rPr)
|
|
203
|
+
out.push(styles.defaultParagraph.rPr);
|
|
204
|
+
if (styles.docDefaultsRPr)
|
|
205
|
+
out.push(styles.docDefaultsRPr);
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
/** The paragraph property holders in resolution order. */
|
|
209
|
+
export function paragraphPropertySources(styles, pPr) {
|
|
210
|
+
const out = [];
|
|
211
|
+
if (pPr)
|
|
212
|
+
out.push(pPr);
|
|
213
|
+
const pStyle = pPr
|
|
214
|
+
? attributeOf(pPr.children.find((child) => child.local === "pStyle"), "w:val")
|
|
215
|
+
: undefined;
|
|
216
|
+
const chain = styles.chain(pStyle ?? styles.defaultParagraph?.id);
|
|
217
|
+
for (const record of chain)
|
|
218
|
+
if (record.pPr)
|
|
219
|
+
out.push(record.pPr);
|
|
220
|
+
if (pStyle &&
|
|
221
|
+
styles.defaultParagraph &&
|
|
222
|
+
!chain.includes(styles.defaultParagraph) &&
|
|
223
|
+
styles.defaultParagraph.pPr)
|
|
224
|
+
out.push(styles.defaultParagraph.pPr);
|
|
225
|
+
if (styles.docDefaultsPPr)
|
|
226
|
+
out.push(styles.docDefaultsPPr);
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
function attributeOf(node, name) {
|
|
230
|
+
return node?.attributes.find((attribute) => attribute.name === name)?.value;
|
|
231
|
+
}
|
|
232
|
+
/** The first holder that has a `name` child, with that child. */
|
|
233
|
+
function firstChild(sources, name) {
|
|
234
|
+
for (const source of sources) {
|
|
235
|
+
const child = source.children.find((candidate) => candidate.local === name && candidate.namespace === W_NS);
|
|
236
|
+
if (child)
|
|
237
|
+
return child;
|
|
238
|
+
}
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
export function resolveTextStyle(styles, pPr, rPr) {
|
|
242
|
+
const sources = runPropertySources(styles, pPr, rPr);
|
|
243
|
+
const toggle = (name) => {
|
|
244
|
+
const node = firstChild(sources, name);
|
|
245
|
+
return node ? isOn(attributeOf(node, "w:val")) : false;
|
|
246
|
+
};
|
|
247
|
+
const sz = firstChild(sources, "sz");
|
|
248
|
+
const half = Number(sz ? attributeOf(sz, "w:val") : NaN);
|
|
249
|
+
const underlineNode = firstChild(sources, "u");
|
|
250
|
+
const underline = underlineNode
|
|
251
|
+
? (attributeOf(underlineNode, "w:val") ?? "single") !== "none"
|
|
252
|
+
: false;
|
|
253
|
+
const highlightNode = firstChild(sources, "highlight");
|
|
254
|
+
const highlight = highlightNode
|
|
255
|
+
? attributeOf(highlightNode, "w:val")
|
|
256
|
+
: undefined;
|
|
257
|
+
return {
|
|
258
|
+
fontFamily: resolveFont(sources, styles.fonts),
|
|
259
|
+
fontSize: Number.isFinite(half) && half > 0 ? half / 2 : 10,
|
|
260
|
+
bold: toggle("b"),
|
|
261
|
+
italic: toggle("i"),
|
|
262
|
+
underline,
|
|
263
|
+
color: resolveColor(firstChild(sources, "color")),
|
|
264
|
+
...(highlight && HIGHLIGHTS.has(highlight) && highlight !== "none"
|
|
265
|
+
? { highlight: highlight }
|
|
266
|
+
: {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function resolveFont(sources, fonts) {
|
|
270
|
+
for (const source of sources) {
|
|
271
|
+
const rFonts = source.children.find((child) => child.local === "rFonts" && child.namespace === W_NS);
|
|
272
|
+
if (!rFonts)
|
|
273
|
+
continue;
|
|
274
|
+
const theme = attributeOf(rFonts, "w:asciiTheme") ??
|
|
275
|
+
attributeOf(rFonts, "w:hAnsiTheme");
|
|
276
|
+
if (theme)
|
|
277
|
+
return theme.startsWith("major") ? fonts.major : fonts.minor;
|
|
278
|
+
const face = attributeOf(rFonts, "w:ascii") ?? attributeOf(rFonts, "w:hAnsi");
|
|
279
|
+
if (face)
|
|
280
|
+
return face;
|
|
281
|
+
}
|
|
282
|
+
return fonts.minor;
|
|
283
|
+
}
|
|
284
|
+
function resolveColor(node) {
|
|
285
|
+
if (!node)
|
|
286
|
+
return "auto";
|
|
287
|
+
const theme = attributeOf(node, "w:themeColor");
|
|
288
|
+
if (theme)
|
|
289
|
+
return { theme };
|
|
290
|
+
const value = attributeOf(node, "w:val");
|
|
291
|
+
return value && /^[0-9A-Fa-f]{6}$/.test(value)
|
|
292
|
+
? `#${value.toUpperCase()}`
|
|
293
|
+
: "auto";
|
|
294
|
+
}
|
|
295
|
+
export function alignOf(value) {
|
|
296
|
+
switch (value) {
|
|
297
|
+
case "left":
|
|
298
|
+
case "start":
|
|
299
|
+
return "left";
|
|
300
|
+
case "center":
|
|
301
|
+
return "center";
|
|
302
|
+
case "right":
|
|
303
|
+
case "end":
|
|
304
|
+
return "right";
|
|
305
|
+
case "both":
|
|
306
|
+
case "distribute":
|
|
307
|
+
case "thaiDistribute":
|
|
308
|
+
case "lowKashida":
|
|
309
|
+
case "mediumKashida":
|
|
310
|
+
case "highKashida":
|
|
311
|
+
return "justify";
|
|
312
|
+
default:
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
export function resolveParagraphStyle(styles, pPr) {
|
|
317
|
+
const sources = paragraphPropertySources(styles, pPr);
|
|
318
|
+
const styleId = pPr
|
|
319
|
+
? attributeOf(pPr.children.find((child) => child.local === "pStyle"), "w:val")
|
|
320
|
+
: undefined;
|
|
321
|
+
const jc = firstChild(sources, "jc");
|
|
322
|
+
const numPr = firstChild(sources, "numPr");
|
|
323
|
+
// Spacing attributes inherit one by one: a style that sets `w:before`
|
|
324
|
+
// keeps the base style's `w:after` and `w:line`.
|
|
325
|
+
const spacings = sources
|
|
326
|
+
.map((source) => source.children.find((child) => child.local === "spacing" && child.namespace === W_NS))
|
|
327
|
+
.filter((node) => node !== undefined);
|
|
328
|
+
const spacingAttribute = (name) => {
|
|
329
|
+
for (const node of spacings) {
|
|
330
|
+
const value = attributeOf(node, name);
|
|
331
|
+
if (value !== undefined)
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
};
|
|
336
|
+
const twips = (name) => {
|
|
337
|
+
const value = Number(spacingAttribute(name) ?? NaN);
|
|
338
|
+
return Number.isFinite(value) ? value / 20 : undefined;
|
|
339
|
+
};
|
|
340
|
+
const before = twips("w:before");
|
|
341
|
+
const after = twips("w:after");
|
|
342
|
+
const lineHolder = spacings.find((node) => attributeOf(node, "w:line") !== undefined);
|
|
343
|
+
const lineRaw = Number(lineHolder ? attributeOf(lineHolder, "w:line") : NaN);
|
|
344
|
+
const lineRule = lineHolder
|
|
345
|
+
? (attributeOf(lineHolder, "w:lineRule") ?? "auto")
|
|
346
|
+
: undefined;
|
|
347
|
+
const line = Number.isFinite(lineRaw)
|
|
348
|
+
? lineRule === "auto"
|
|
349
|
+
? lineRaw / 240
|
|
350
|
+
: lineRaw / 20
|
|
351
|
+
: undefined;
|
|
352
|
+
const out = {
|
|
353
|
+
...(before === undefined ? {} : { before }),
|
|
354
|
+
...(after === undefined ? {} : { after }),
|
|
355
|
+
...(line === undefined ? {} : { line }),
|
|
356
|
+
...(line === undefined ||
|
|
357
|
+
(lineRule !== "auto" && lineRule !== "exact" && lineRule !== "atLeast")
|
|
358
|
+
? {}
|
|
359
|
+
: { lineRule }),
|
|
360
|
+
};
|
|
361
|
+
const numId = Number(numPr
|
|
362
|
+
? attributeOf(numPr.children.find((c) => c.local === "numId"), "w:val")
|
|
363
|
+
: NaN);
|
|
364
|
+
const level = Number(numPr
|
|
365
|
+
? attributeOf(numPr.children.find((c) => c.local === "ilvl"), "w:val")
|
|
366
|
+
: 0);
|
|
367
|
+
return {
|
|
368
|
+
...(styleId ? { styleId } : {}),
|
|
369
|
+
align: alignOf(jc ? attributeOf(jc, "w:val") : undefined) ?? "left",
|
|
370
|
+
spacing: out,
|
|
371
|
+
...(Number.isInteger(numId) && numId > 0
|
|
372
|
+
? { numbering: { numId, level: Number.isInteger(level) ? level : 0 } }
|
|
373
|
+
: {}),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DocxOperationHandler } from "./operations.js";
|
|
2
|
+
import type { DocxInsertTableOperation, DocxSetTableCellOperation } from "./types.js";
|
|
3
|
+
export declare const insertTableHandler: DocxOperationHandler<DocxInsertTableOperation>;
|
|
4
|
+
export declare const setTableCellHandler: DocxOperationHandler<DocxSetTableCellOperation>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { patches } from "../ooxml/patch.js";
|
|
2
|
+
import { W_NS } from "./ids.js";
|
|
3
|
+
import { LINE_BREAK } from "./text.js";
|
|
4
|
+
import { replacedParagraph } from "./text-ops.js";
|
|
5
|
+
import { namespacePatches, normalizeText, paragraphXml, runContentXml, runXml, textProblem, } from "./write.js";
|
|
6
|
+
/*
|
|
7
|
+
* Tables: a new `w:tbl` next to a paragraph or table of the body, with a
|
|
8
|
+
* grid over the section's content width, one paragraph per cell and a
|
|
9
|
+
* paragraph after it where the body needs one; a cell's text replaced in
|
|
10
|
+
* its first paragraph with the cell's other paragraphs removed.
|
|
11
|
+
*/
|
|
12
|
+
/** US Letter with one-inch margins: the content width Word assumes without a section. */
|
|
13
|
+
const DEFAULT_CONTENT_WIDTH = 12240 - 2 * 1440;
|
|
14
|
+
/** Exactly one of `before` and `after`, resolved to a body-level block. */
|
|
15
|
+
function placement(operation, context, issue) {
|
|
16
|
+
const named = [operation.before, operation.after].filter((value) => value !== undefined);
|
|
17
|
+
if (named.length !== 1) {
|
|
18
|
+
issue("/before", "invalid-value", "Exactly one of before and after names the reference element");
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const side = operation.before !== undefined ? "before" : "after";
|
|
22
|
+
const id = operation[side];
|
|
23
|
+
const record = context.model.byId.get(id);
|
|
24
|
+
if (!record || (record.kind !== "paragraph" && record.kind !== "table")) {
|
|
25
|
+
issue(`/${side}`, record ? "invalid-target" : "unknown-target", record
|
|
26
|
+
? `Element ${id} is not a paragraph or a table`
|
|
27
|
+
: `No element ${id}`);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (record.kind === "paragraph" && record.table) {
|
|
31
|
+
issue(`/${side}`, "invalid-target", "A table cannot be placed inside a cell");
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return { record, side };
|
|
35
|
+
}
|
|
36
|
+
/** The content width, in twentieths of a point, of the section a block belongs to. */
|
|
37
|
+
function contentWidth(model, node, side) {
|
|
38
|
+
const part = model.document;
|
|
39
|
+
const geometry = (sectPr) => {
|
|
40
|
+
if (!sectPr)
|
|
41
|
+
return undefined;
|
|
42
|
+
const read = (local, name) => {
|
|
43
|
+
const holder = sectPr.children.find((child) => child.local === local && child.namespace === W_NS);
|
|
44
|
+
const value = Number(holder ? part.attribute(holder, name) : NaN);
|
|
45
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
46
|
+
};
|
|
47
|
+
const width = read("pgSz", "w:w");
|
|
48
|
+
if (width === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
return (width -
|
|
51
|
+
(read("pgMar", "w:left") ?? 1440) -
|
|
52
|
+
(read("pgMar", "w:right") ?? 1440));
|
|
53
|
+
};
|
|
54
|
+
// The section a block belongs to ends at the next paragraph carrying a
|
|
55
|
+
// w:sectPr, else at the body's own; a table placed after a paragraph
|
|
56
|
+
// that ends a section lands in the section after it.
|
|
57
|
+
let passed = false;
|
|
58
|
+
for (const block of model.blocks) {
|
|
59
|
+
const own = block.node === node;
|
|
60
|
+
if (own)
|
|
61
|
+
passed = true;
|
|
62
|
+
if (own && side === "after")
|
|
63
|
+
continue;
|
|
64
|
+
if (passed && block.kind === "paragraph" && block.sectPr) {
|
|
65
|
+
const width = geometry(block.sectPr);
|
|
66
|
+
if (width !== undefined)
|
|
67
|
+
return width;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return geometry(model.bodySectPr) ?? DEFAULT_CONTENT_WIDTH;
|
|
72
|
+
}
|
|
73
|
+
/** Column widths in twentieths of a point from relative weights over `total`. */
|
|
74
|
+
function columnWidths(weights, total) {
|
|
75
|
+
const sum = weights.reduce((a, b) => a + b, 0);
|
|
76
|
+
const widths = weights.map((weight) => Math.floor((weight / sum) * total));
|
|
77
|
+
const remainder = total - widths.reduce((a, b) => a + b, 0);
|
|
78
|
+
widths[widths.length - 1] += remainder;
|
|
79
|
+
return widths;
|
|
80
|
+
}
|
|
81
|
+
/** `w:tblPr`: the TableGrid style when the document defines it, else single borders. */
|
|
82
|
+
function tablePropertiesXml(model) {
|
|
83
|
+
const grid = model.styles.styles.get("TableGrid");
|
|
84
|
+
const look = '<w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/>';
|
|
85
|
+
if (grid && grid.type === "table")
|
|
86
|
+
return `<w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="0" w:type="auto"/>${look}</w:tblPr>`;
|
|
87
|
+
const edge = (name) => `<w:${name} w:val="single" w:sz="4" w:space="0" w:color="auto"/>`;
|
|
88
|
+
return `<w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders>${["top", "left", "bottom", "right", "insideH", "insideV"].map(edge).join("")}</w:tblBorders>${look}</w:tblPr>`;
|
|
89
|
+
}
|
|
90
|
+
/** The text of a cell as one paragraph: newlines become line breaks. */
|
|
91
|
+
function cellRunXml(text) {
|
|
92
|
+
return runXml("", runContentXml(normalizeText(text).replaceAll("\n", LINE_BREAK)));
|
|
93
|
+
}
|
|
94
|
+
function commit(context, items, result) {
|
|
95
|
+
const part = context.model.document;
|
|
96
|
+
const transaction = context.pkg.transaction();
|
|
97
|
+
transaction.patch(part, [...namespacePatches(part), ...items]);
|
|
98
|
+
return transaction
|
|
99
|
+
.commit()
|
|
100
|
+
.then((change) => ({ ...result, warnings: change.warnings }));
|
|
101
|
+
}
|
|
102
|
+
export const insertTableHandler = {
|
|
103
|
+
async validate(operation, context, issue) {
|
|
104
|
+
placement(operation, context, issue);
|
|
105
|
+
const columns = operation.rows[0]?.length ?? 0;
|
|
106
|
+
if (operation.rows.some((row) => row.length !== columns))
|
|
107
|
+
issue("/rows", "invalid-value", "Every row needs the same number of cells");
|
|
108
|
+
if (operation.columnWidths && operation.columnWidths.length !== columns)
|
|
109
|
+
issue("/columnWidths", "invalid-value", "One weight per column is needed");
|
|
110
|
+
operation.rows.forEach((row, rowIndex) => row.forEach((cell, columnIndex) => {
|
|
111
|
+
const problem = textProblem(cell);
|
|
112
|
+
if (problem)
|
|
113
|
+
issue(`/rows/${rowIndex}/${columnIndex}`, "invalid-text", `The text holds ${problem}`);
|
|
114
|
+
}));
|
|
115
|
+
},
|
|
116
|
+
async apply(operation, context) {
|
|
117
|
+
const { model } = context;
|
|
118
|
+
const { record, side } = placement(operation, context, () => { });
|
|
119
|
+
const part = model.document;
|
|
120
|
+
const node = record.node;
|
|
121
|
+
const widths = columnWidths(operation.columnWidths ?? operation.rows[0].map(() => 1), contentWidth(model, node, side));
|
|
122
|
+
const createdIds = [];
|
|
123
|
+
const cellIds = [];
|
|
124
|
+
const rows = operation.rows
|
|
125
|
+
.map((row) => `<w:tr>${row
|
|
126
|
+
.map((cell, column) => {
|
|
127
|
+
const id = context.freshParagraphId();
|
|
128
|
+
cellIds.push(`p:${id}`);
|
|
129
|
+
return `<w:tc><w:tcPr><w:tcW w:w="${widths[column]}" w:type="dxa"/></w:tcPr>${paragraphXml(undefined, id, "", cellRunXml(cell))}</w:tc>`;
|
|
130
|
+
})
|
|
131
|
+
.join("")}</w:tr>`)
|
|
132
|
+
.join("");
|
|
133
|
+
const table = `<w:tbl>${tablePropertiesXml(model)}<w:tblGrid>${widths
|
|
134
|
+
.map((width) => `<w:gridCol w:w="${width}"/>`)
|
|
135
|
+
.join("")}</w:tblGrid>${rows}</w:tbl>`;
|
|
136
|
+
createdIds.push(`tbl:${cellIds[0].slice(2)}`, ...cellIds);
|
|
137
|
+
// A paragraph must follow the table when the next sibling would be a
|
|
138
|
+
// table or the body's section properties.
|
|
139
|
+
const siblings = model.blocks;
|
|
140
|
+
const index = siblings.indexOf(record);
|
|
141
|
+
const next = side === "before" ? record : siblings[index + 1];
|
|
142
|
+
let trailing = "";
|
|
143
|
+
if (!next || next.kind === "table") {
|
|
144
|
+
const id = context.freshParagraphId();
|
|
145
|
+
createdIds.push(`p:${id}`);
|
|
146
|
+
trailing = paragraphXml(undefined, id, "", "");
|
|
147
|
+
}
|
|
148
|
+
return commit(context, [
|
|
149
|
+
side === "before"
|
|
150
|
+
? patches.insertBefore(part, node, table)
|
|
151
|
+
: patches.insertAfter(part, node, table),
|
|
152
|
+
...(trailing
|
|
153
|
+
? [
|
|
154
|
+
side === "before"
|
|
155
|
+
? patches.insertBefore(part, node, trailing)
|
|
156
|
+
: patches.insertAfter(part, node, trailing),
|
|
157
|
+
]
|
|
158
|
+
: []),
|
|
159
|
+
], { createdIds, reflowFrom: record.id });
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
export const setTableCellHandler = {
|
|
163
|
+
async validate(operation, context, issue) {
|
|
164
|
+
const problem = textProblem(normalizeText(operation.text));
|
|
165
|
+
if (problem)
|
|
166
|
+
issue("/text", "invalid-text", `The text holds ${problem}`);
|
|
167
|
+
const record = context.model.byId.get(operation.target);
|
|
168
|
+
if (!record || record.kind !== "table") {
|
|
169
|
+
issue("/target", record ? "invalid-target" : "unknown-target", record
|
|
170
|
+
? `Element ${operation.target} is not a table`
|
|
171
|
+
: `No element ${operation.target}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const row = record.rows[operation.row];
|
|
175
|
+
if (!row) {
|
|
176
|
+
issue("/row", "range", `The table has ${record.rows.length} rows`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const cell = row[operation.column];
|
|
180
|
+
if (!cell) {
|
|
181
|
+
issue("/column", "range", `Row ${operation.row} has ${row.length} cells`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const first = cell.paragraphs[0];
|
|
185
|
+
if (!first)
|
|
186
|
+
issue("/target", "invalid-target", "The cell holds no paragraph");
|
|
187
|
+
else if (first.readOnlyReason)
|
|
188
|
+
issue("/target", "invalid-target", `The cell's paragraph is read-only (${first.readOnlyReason})`);
|
|
189
|
+
},
|
|
190
|
+
async apply(operation, context) {
|
|
191
|
+
const { model } = context;
|
|
192
|
+
const record = model.byId.get(operation.target);
|
|
193
|
+
const cell = record.rows[operation.row][operation.column];
|
|
194
|
+
const [first, ...rest] = cell.paragraphs;
|
|
195
|
+
const text = normalizeText(operation.text).replaceAll("\n", LINE_BREAK);
|
|
196
|
+
const replaced = replacedParagraph(context, first, 0, first.text.text.length, text);
|
|
197
|
+
const part = model.document;
|
|
198
|
+
const items = [...replaced.items];
|
|
199
|
+
const removedIds = [...replaced.removedIds];
|
|
200
|
+
const removedParagraphIds = [...replaced.removedParagraphIds];
|
|
201
|
+
for (const paragraph of rest) {
|
|
202
|
+
items.push(patches.removeElement(part, paragraph.node));
|
|
203
|
+
removedIds.push(paragraph.elementId, ...paragraph.inlines.map((inline) => inline.elementId));
|
|
204
|
+
removedParagraphIds.push(paragraph.id);
|
|
205
|
+
for (const nested of part.findAll("p", paragraph.node)) {
|
|
206
|
+
const id = model.paragraphIds.get(nested);
|
|
207
|
+
if (id && nested !== paragraph.node)
|
|
208
|
+
removedParagraphIds.push(id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return commit(context, items, {
|
|
212
|
+
createdIds: replaced.createdIds,
|
|
213
|
+
...(removedIds.length > 0 ? { removedIds } : {}),
|
|
214
|
+
...(removedParagraphIds.length > 0 ? { removedParagraphIds } : {}),
|
|
215
|
+
...(model.unauthoredSet.has(first.id) ? { stamped: [first.id] } : {}),
|
|
216
|
+
reflowFrom: record.id,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type XmlPatch } from "../ooxml/patch.js";
|
|
2
|
+
import type { ParagraphRecord } from "./model.js";
|
|
3
|
+
import type { DocxOperationContext, DocxOperationHandler } from "./operations.js";
|
|
4
|
+
import type { DocxReplaceTextOperation, DocxSetParagraphStyleOperation, DocxSetTextStyleOperation } from "./types.js";
|
|
5
|
+
export declare const replaceTextHandler: DocxOperationHandler<DocxReplaceTextOperation>;
|
|
6
|
+
/**
|
|
7
|
+
* The patches that replace `[start, end)` of a paragraph's text with
|
|
8
|
+
* `text`, newlines splitting it into new paragraphs after it: the first
|
|
9
|
+
* paragraph rebuilt in place, the others inserted with fresh ids.
|
|
10
|
+
*/
|
|
11
|
+
export declare function replacedParagraph(context: DocxOperationContext, record: ParagraphRecord, start: number, end: number, text: string): {
|
|
12
|
+
items: XmlPatch[];
|
|
13
|
+
createdIds: string[];
|
|
14
|
+
removedIds: string[];
|
|
15
|
+
removedParagraphIds: string[];
|
|
16
|
+
};
|
|
17
|
+
export declare const setTextStyleHandler: DocxOperationHandler<DocxSetTextStyleOperation>;
|
|
18
|
+
export declare const setParagraphStyleHandler: DocxOperationHandler<DocxSetParagraphStyleOperation>;
|