@docx4j/core-ts 0.1.0 → 0.1.2
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/README.md +410 -10
- package/dist/model/content/Body.d.mts +109 -4
- package/dist/model/content/Body.mjs +300 -29
- package/dist/model/content/Comment.d.mts +78 -0
- package/dist/model/content/Comment.mjs +429 -0
- package/dist/model/content/ContentControl.d.mts +173 -0
- package/dist/model/content/ContentControl.mjs +638 -0
- package/dist/model/content/Font.d.mts +48 -8
- package/dist/model/content/Font.mjs +55 -13
- package/dist/model/content/InlinePicture.d.mts +103 -0
- package/dist/model/content/InlinePicture.mjs +361 -0
- package/dist/model/content/List.d.mts +253 -0
- package/dist/model/content/List.mjs +778 -0
- package/dist/model/content/Paragraph.d.mts +213 -16
- package/dist/model/content/Paragraph.mjs +611 -57
- package/dist/model/content/Range.d.mts +48 -4
- package/dist/model/content/Range.mjs +161 -4
- package/dist/model/content/Table.d.mts +130 -0
- package/dist/model/content/Table.mjs +429 -0
- package/dist/model/content/TrackedChange.d.mts +96 -0
- package/dist/model/content/TrackedChange.mjs +352 -0
- package/dist/model/content/comments.d.mts +122 -0
- package/dist/model/content/comments.mjs +248 -0
- package/dist/model/content/index.d.mts +13 -3
- package/dist/model/content/index.mjs +15 -3
- package/dist/model/content/ooxml.d.mts +17 -0
- package/dist/model/content/ooxml.mjs +214 -0
- package/dist/model/content/styles.d.mts +21 -0
- package/dist/model/content/styles.mjs +86 -0
- package/dist/model/content/tracking.d.mts +104 -0
- package/dist/model/content/tracking.mjs +313 -0
- package/dist/model/content/tree.d.mts +56 -9
- package/dist/model/content/tree.mjs +104 -31
- package/dist/model/customxml/CustomXmlPart.d.mts +158 -0
- package/dist/model/customxml/CustomXmlPart.mjs +415 -0
- package/dist/model/customxml/CustomXmlPartCollection.d.mts +68 -0
- package/dist/model/customxml/CustomXmlPartCollection.mjs +183 -0
- package/dist/model/customxml/XmlMapping.d.mts +46 -0
- package/dist/model/customxml/XmlMapping.mjs +98 -0
- package/dist/model/customxml/bindings.d.mts +41 -0
- package/dist/model/customxml/bindings.mjs +230 -0
- package/dist/model/customxml/index.d.mts +7 -0
- package/dist/model/customxml/index.mjs +9 -0
- package/dist/model/customxml/insert.d.mts +20 -0
- package/dist/model/customxml/insert.mjs +22 -0
- package/dist/model/customxml/kinds.d.mts +103 -0
- package/dist/model/customxml/kinds.mjs +267 -0
- package/dist/model/customxml/xpath.d.mts +52 -0
- package/dist/model/customxml/xpath.mjs +205 -0
- package/dist/model/fonts/CJKToEnglish.d.mts +2 -0
- package/dist/model/fonts/CJKToEnglish.mjs +16 -0
- package/dist/model/fonts/FontFallback.d.mts +58 -0
- package/dist/model/fonts/FontFallback.mjs +219 -0
- package/dist/model/fonts/IdentityPlusMapper.d.mts +7 -0
- package/dist/model/fonts/IdentityPlusMapper.mjs +19 -0
- package/dist/model/fonts/LanguageTagToScriptMapping.d.mts +7 -0
- package/dist/model/fonts/LanguageTagToScriptMapping.mjs +67 -0
- package/dist/model/fonts/Mapper.d.mts +190 -0
- package/dist/model/fonts/Mapper.mjs +481 -0
- package/dist/model/fonts/PhysicalFont.d.mts +34 -0
- package/dist/model/fonts/PhysicalFont.mjs +62 -0
- package/dist/model/fonts/RunFontSelector.d.mts +194 -0
- package/dist/model/fonts/RunFontSelector.mjs +441 -0
- package/dist/model/fonts/ThemeFonts.d.mts +17 -0
- package/dist/model/fonts/ThemeFonts.mjs +76 -0
- package/dist/model/fonts/defaultTheme.d.mts +37 -0
- package/dist/model/fonts/defaultTheme.mjs +57 -0
- package/dist/model/fonts/families.generated.d.mts +11 -0
- package/dist/model/fonts/families.generated.mjs +17 -0
- package/dist/model/fonts/fontsInUse.d.mts +39 -0
- package/dist/model/fonts/fontsInUse.mjs +183 -0
- package/dist/model/fonts/index.d.mts +14 -0
- package/dist/model/fonts/index.mjs +29 -0
- package/dist/model/fonts/lookup.d.mts +5 -0
- package/dist/model/fonts/lookup.mjs +12 -0
- package/dist/model/fonts/registry.d.mts +31 -0
- package/dist/model/fonts/registry.mjs +103 -0
- package/dist/model/fonts/scripts.d.mts +2 -0
- package/dist/model/fonts/scripts.mjs +78 -0
- package/dist/model/fonts/substitutions.generated.d.mts +30 -0
- package/dist/model/fonts/substitutions.generated.mjs +12 -0
- package/dist/model/fonts/themes.generated.d.mts +6 -0
- package/dist/model/fonts/themes.generated.mjs +11 -0
- package/dist/model/index.d.mts +5 -0
- package/dist/model/index.mjs +13 -1
- package/dist/model/listnumbering/Emulator.d.mts +187 -0
- package/dist/model/listnumbering/Emulator.mjs +332 -0
- package/dist/model/listnumbering/definitions.d.mts +251 -0
- package/dist/model/listnumbering/definitions.mjs +571 -0
- package/dist/model/listnumbering/formats.d.mts +25 -0
- package/dist/model/listnumbering/formats.mjs +320 -0
- package/dist/model/listnumbering/index.d.mts +4 -0
- package/dist/model/listnumbering/index.mjs +15 -0
- package/dist/model/listnumbering/state.d.mts +97 -0
- package/dist/model/listnumbering/state.mjs +179 -0
- package/dist/model/properties/PropertyResolver.d.mts +216 -0
- package/dist/model/properties/PropertyResolver.mjs +524 -0
- package/dist/model/properties/catalogue.d.mts +179 -0
- package/dist/model/properties/catalogue.mjs +860 -0
- package/dist/model/properties/index.d.mts +4 -0
- package/dist/model/properties/index.mjs +9 -0
- package/dist/model/properties/log.d.mts +7 -0
- package/dist/model/properties/log.mjs +25 -0
- package/dist/model/properties/styleUtil.d.mts +112 -0
- package/dist/model/properties/styleUtil.mjs +359 -0
- package/dist/node/index.d.mts +1 -0
- package/dist/node/index.mjs +12 -0
- package/dist/office-js/document.d.mts +111 -0
- package/dist/office-js/document.mjs +267 -0
- package/dist/office-js/enums.d.mts +187 -0
- package/dist/office-js/enums.mjs +176 -0
- package/dist/office-js/errors.d.mts +29 -0
- package/dist/office-js/errors.mjs +39 -0
- package/dist/office-js/extras.d.mts +5 -0
- package/dist/office-js/extras.mjs +29 -0
- package/dist/office-js/fragment.d.mts +10 -0
- package/dist/office-js/fragment.mjs +23 -0
- package/dist/office-js/index.d.mts +183 -0
- package/dist/office-js/index.mjs +54 -0
- package/dist/office-js/proxy.d.mts +72 -0
- package/dist/office-js/proxy.mjs +279 -0
- package/dist/office-js/run.d.mts +32 -0
- package/dist/office-js/run.mjs +66 -0
- package/dist/office-js/supported.d.mts +7 -0
- package/dist/office-js/supported.generated.d.mts +1 -0
- package/dist/office-js/supported.generated.mjs +235 -0
- package/dist/office-js/supported.mjs +53 -0
- package/dist/office-js/toApiScript.d.mts +25 -0
- package/dist/office-js/toApiScript.mjs +394 -0
- package/dist/opc/ContentTypes.d.mts +11 -0
- package/dist/opc/ContentTypes.mjs +14 -0
- package/dist/opc/DirectoryPartStore.d.mts +47 -0
- package/dist/opc/DirectoryPartStore.mjs +123 -0
- package/dist/opc/exceptions.d.mts +30 -0
- package/dist/opc/exceptions.mjs +38 -0
- package/dist/opc/index.d.mts +1 -1
- package/dist/opc/index.mjs +1 -1
- package/dist/packages/OpcPackage.d.mts +19 -1
- package/dist/packages/OpcPackage.mjs +29 -0
- package/dist/packages/PresentationMLPackage.d.mts +71 -1
- package/dist/packages/PresentationMLPackage.mjs +179 -0
- package/dist/packages/SpreadsheetMLPackage.d.mts +30 -1
- package/dist/packages/SpreadsheetMLPackage.mjs +78 -0
- package/dist/packages/WordprocessingMLPackage.d.mts +124 -2
- package/dist/packages/WordprocessingMLPackage.mjs +309 -2
- package/dist/packages/index.d.mts +1 -1
- package/dist/packages/index.mjs +1 -1
- package/dist/parts/DefaultXmlPart.d.mts +18 -0
- package/dist/parts/DefaultXmlPart.mjs +34 -5
- package/dist/parts/Namespaces.d.mts +18 -0
- package/dist/parts/Namespaces.mjs +20 -0
- package/dist/parts/Part.d.mts +6 -0
- package/dist/parts/Part.mjs +13 -0
- package/dist/parts/PartRegistry.mjs +13 -1
- package/dist/parts/XmlPart.d.mts +33 -0
- package/dist/parts/XmlPart.mjs +105 -2
- package/dist/parts/pml/defaults.d.mts +6 -0
- package/dist/parts/pml/defaults.mjs +53 -0
- package/dist/parts/pml/index.d.mts +66 -3
- package/dist/parts/pml/index.mjs +206 -7
- package/dist/parts/pml/placeholders.d.mts +71 -0
- package/dist/parts/pml/placeholders.mjs +141 -0
- package/dist/parts/sml/index.d.mts +80 -1
- package/dist/parts/sml/index.mjs +154 -1
- package/dist/parts/wml/comments.d.mts +1 -0
- package/dist/parts/wml/comments.mjs +179 -0
- package/dist/parts/wml/defaultNumbering.d.mts +1 -0
- package/dist/parts/wml/defaultNumbering.mjs +216 -0
- package/dist/parts/wml/index.d.mts +121 -0
- package/dist/parts/wml/index.mjs +291 -0
- package/dist/xml/dom.mjs +1 -2
- package/package.json +25 -4
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Body } from '../model/content/Body.mjs';
|
|
2
|
+
import { Paragraph } from '../model/content/Paragraph.mjs';
|
|
3
|
+
import { Range } from '../model/content/Range.mjs';
|
|
4
|
+
import { type Element } from '../model/content/tree.mjs';
|
|
5
|
+
export interface ApiScriptOptions {
|
|
6
|
+
/** The variable the script inserts into; default `'body'`. */
|
|
7
|
+
variable?: string;
|
|
8
|
+
/** The insert location; default `'End'`. */
|
|
9
|
+
location?: 'Start' | 'End';
|
|
10
|
+
/** Prefix for every line; default `''`. */
|
|
11
|
+
indent?: string;
|
|
12
|
+
}
|
|
13
|
+
/** What `toApiScript` takes: a view, an element of the tree, or several. */
|
|
14
|
+
export type ApiScriptTarget = Body | Paragraph | Range | Element | Element[];
|
|
15
|
+
/**
|
|
16
|
+
* The content-API script that reproduces what it is given, as TypeScript against `body`: the verbs
|
|
17
|
+
* where they exist (`insertParagraph`, `style`, `alignment`, the indents and spacing, `insertText`
|
|
18
|
+
* with `Range.font` per run, `insertBreak`, `insertTable`), and `insertXml` with the marshalled
|
|
19
|
+
* fragment for the rest. Emitted for reveal codes, for teaching the API by example, and for an
|
|
20
|
+
* agent that wants the calls rather than the tree.
|
|
21
|
+
*
|
|
22
|
+
* The script is a sequence of statements over `body`; it uses `await` for the `insertXml` lines,
|
|
23
|
+
* so run it in an async function (`new Function('body', 'return (async () => {' + script + '})()')`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function toApiScript(target: ApiScriptTarget, options?: ApiScriptOptions): Promise<string>;
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// `toApiScript` (CR-002 section 3.10): the content-API calls that reproduce a paragraph, a range,
|
|
2
|
+
// a table or a body, emitted as TypeScript against `body`. The counterpart of the objects
|
|
3
|
+
// package's `toSource`, which emits the factory calls that build the element; this one stays in
|
|
4
|
+
// the vocabulary an add-in author (or an agent writing one) reads, and falls back to `insertXml`
|
|
5
|
+
// with the marshalled fragment for what the verbs cannot express.
|
|
6
|
+
import { marshalString } from '@docx4j/generated-objects-ts';
|
|
7
|
+
import { readRunOptions } from '@docx4j/generated-objects-ts/builders/wml';
|
|
8
|
+
import { Docx4JException } from '../opc/exceptions.mjs';
|
|
9
|
+
import { Body } from '../model/content/Body.mjs';
|
|
10
|
+
import { Paragraph } from '../model/content/Paragraph.mjs';
|
|
11
|
+
import { Range } from '../model/content/Range.mjs';
|
|
12
|
+
import { isElement, typeNameOf, textOf } from '../model/content/tree.mjs';
|
|
13
|
+
import { paragraphElementOfRange } from './fragment.mjs';
|
|
14
|
+
import { builtInOf, displayNameOf } from '../model/content/styles.mjs';
|
|
15
|
+
const TWIPS_PER_POINT = 20;
|
|
16
|
+
/** Paragraph properties the verbs express; anything else sends the paragraph to `insertXml`. */
|
|
17
|
+
const PPR_KEYS = new Set(['TYPE_NAME', 'PARENT', 'pStyle', 'jc', 'ind', 'spacing', 'outlineLvl', 'numPr']);
|
|
18
|
+
const NUMPR_KEYS = new Set(['TYPE_NAME', 'PARENT', 'numId', 'ilvl']);
|
|
19
|
+
/** The `member` of a property pair that is a **call**, not an assignment (`attachToList`). */
|
|
20
|
+
const CALL = '()';
|
|
21
|
+
const IND_KEYS = new Set(['TYPE_NAME', 'PARENT', 'left', 'right', 'firstLine', 'hanging']);
|
|
22
|
+
const SPACING_KEYS = new Set(['TYPE_NAME', 'PARENT', 'before', 'after', 'line', 'lineRule']);
|
|
23
|
+
/** Run properties the `Font` members express (the objects package's applyRunOptions vocabulary). */
|
|
24
|
+
const RPR_KEYS = new Set(['TYPE_NAME', 'PARENT', 'b', 'bCs', 'i', 'iCs', 'u', 'strike', 'dstrike', 'vertAlign', 'rFonts', 'sz', 'szCs', 'color', 'highlight']);
|
|
25
|
+
const RFONTS_KEYS = new Set(['TYPE_NAME', 'PARENT', 'ascii', 'hAnsi']);
|
|
26
|
+
/** Markers that carry no content: dropped, since no verb writes them. */
|
|
27
|
+
const IGNORED_TYPES = new Set(['org_docx4j_wml.ProofErr', 'org_docx4j_wml.CTEmpty']);
|
|
28
|
+
const BOOKMARK_TYPES = new Set(['org_docx4j_wml.CTBookmark', 'org_docx4j_wml.CTMarkupRange']);
|
|
29
|
+
/** Table properties `insertTable` can reproduce. */
|
|
30
|
+
const TBLPR_KEYS = new Set(['TYPE_NAME', 'PARENT', 'tblStyle', 'tblW', 'tblLook']);
|
|
31
|
+
/** Marks a construct the verbs cannot express: the caller falls back to `insertXml`. */
|
|
32
|
+
class Unexpressible extends Error {
|
|
33
|
+
}
|
|
34
|
+
function quote(text) {
|
|
35
|
+
const escaped = text
|
|
36
|
+
.replace(/\\/g, '\\\\')
|
|
37
|
+
.replace(/'/g, "\\'")
|
|
38
|
+
.replace(/\n/g, '\\n')
|
|
39
|
+
.replace(/\r/g, '\\r')
|
|
40
|
+
.replace(/\t/g, '\\t');
|
|
41
|
+
return `'${escaped}'`;
|
|
42
|
+
}
|
|
43
|
+
function template(xml) {
|
|
44
|
+
return '`' + xml.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${') + '`';
|
|
45
|
+
}
|
|
46
|
+
function num(value) {
|
|
47
|
+
return String(Math.round(value * 100) / 100);
|
|
48
|
+
}
|
|
49
|
+
function keysOf(value) {
|
|
50
|
+
return Object.keys(value).filter((k) => value[k] !== undefined);
|
|
51
|
+
}
|
|
52
|
+
function within(value, allowed) {
|
|
53
|
+
return keysOf(value).every((k) => allowed.has(k));
|
|
54
|
+
}
|
|
55
|
+
const DEFAULT_FORMAT = readRunOptions(undefined);
|
|
56
|
+
/** The `Font` assignments that turn `from` into `to`; throws when a property cannot be unset. */
|
|
57
|
+
function fontDiff(from, to) {
|
|
58
|
+
const out = [];
|
|
59
|
+
const flags = ['bold', 'italic', 'strikeThrough', 'doubleStrikeThrough', 'subscript', 'superscript'];
|
|
60
|
+
for (const flag of flags)
|
|
61
|
+
if (from[flag] !== to[flag])
|
|
62
|
+
out.push([flag, String(to[flag])]);
|
|
63
|
+
if (from.underline !== to.underline)
|
|
64
|
+
out.push(['underline', quote(to.underline)]);
|
|
65
|
+
if (from.name !== to.name)
|
|
66
|
+
out.push(['name', quote(to.name)]);
|
|
67
|
+
if (from.size !== to.size) {
|
|
68
|
+
// Font.size writes w:sz; there is no size that removes it, so a run that drops a direct size
|
|
69
|
+
// after one that set it is not expressible (CR-002 section 10).
|
|
70
|
+
if (to.size === 0)
|
|
71
|
+
throw new Unexpressible('a run drops a direct font size');
|
|
72
|
+
out.push(['size', num(to.size)]);
|
|
73
|
+
}
|
|
74
|
+
if (from.color !== to.color)
|
|
75
|
+
out.push(['color', quote(to.color)]);
|
|
76
|
+
if (from.highlightColor !== to.highlightColor)
|
|
77
|
+
out.push(['highlightColor', to.highlightColor === null ? 'null' : quote(to.highlightColor)]);
|
|
78
|
+
if (from.style !== to.style)
|
|
79
|
+
throw new Unexpressible('a run style (w:rStyle)');
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/** The emitter: one instance per call, so that variable names are stable and readable. */
|
|
83
|
+
class Emitter {
|
|
84
|
+
constructor(options) {
|
|
85
|
+
var _a, _b, _c;
|
|
86
|
+
this.lines = [];
|
|
87
|
+
this.paragraphs = 0;
|
|
88
|
+
this.ranges = 0;
|
|
89
|
+
this.tables = 0;
|
|
90
|
+
this.variable = (_a = options.variable) !== null && _a !== void 0 ? _a : 'body';
|
|
91
|
+
this.location = (_b = options.location) !== null && _b !== void 0 ? _b : 'End';
|
|
92
|
+
this.indent = (_c = options.indent) !== null && _c !== void 0 ? _c : '';
|
|
93
|
+
}
|
|
94
|
+
script() {
|
|
95
|
+
return this.lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
line(text) {
|
|
98
|
+
this.lines.push(this.indent + text);
|
|
99
|
+
}
|
|
100
|
+
/** A block-level element: a paragraph, a table, or anything else through `insertXml`. */
|
|
101
|
+
async element(element) {
|
|
102
|
+
const typeName = typeNameOf(element);
|
|
103
|
+
if (typeName === 'org_docx4j_wml.P')
|
|
104
|
+
await this.paragraph(element);
|
|
105
|
+
else if (typeName === 'org_docx4j_wml.Tbl')
|
|
106
|
+
await this.table(element);
|
|
107
|
+
else
|
|
108
|
+
await this.fallback(element);
|
|
109
|
+
}
|
|
110
|
+
async paragraph(element) {
|
|
111
|
+
try {
|
|
112
|
+
this.emitParagraph(element.value);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (!(error instanceof Unexpressible))
|
|
116
|
+
throw error;
|
|
117
|
+
await this.fallback(element, error.message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** A range: its text and run formatting, as one paragraph. */
|
|
121
|
+
async range(range) {
|
|
122
|
+
await this.paragraph(paragraphElementOfRange(range));
|
|
123
|
+
}
|
|
124
|
+
emitParagraph(p) {
|
|
125
|
+
const properties = this.paragraphProperties(p);
|
|
126
|
+
const pieces = piecesOf(p);
|
|
127
|
+
const first = pieces[0];
|
|
128
|
+
const plainFirst = first !== undefined && first.kind === 'text' && sameFormat(first.format, DEFAULT_FORMAT);
|
|
129
|
+
// Every font change is checked before a line is emitted, so a paragraph either comes out whole
|
|
130
|
+
// or falls back whole.
|
|
131
|
+
let previous = DEFAULT_FORMAT;
|
|
132
|
+
const diffs = [];
|
|
133
|
+
for (const piece of pieces.slice(plainFirst ? 1 : 0)) {
|
|
134
|
+
if (piece.kind === 'break') {
|
|
135
|
+
diffs.push(undefined);
|
|
136
|
+
previous = DEFAULT_FORMAT;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
diffs.push(fontDiff(previous, piece.format));
|
|
140
|
+
previous = piece.format;
|
|
141
|
+
}
|
|
142
|
+
if (properties.length === 0 && plainFirst && pieces.length === 1) {
|
|
143
|
+
this.line(`${this.variable}.insertParagraph(${quote(first.text)}, '${this.location}');`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const name = `p${++this.paragraphs}`;
|
|
147
|
+
this.line(`const ${name} = ${this.variable}.insertParagraph(${quote(plainFirst ? first.text : '')}, '${this.location}');`);
|
|
148
|
+
for (const [member, value] of properties)
|
|
149
|
+
this.line(member === CALL ? `${name}.${value};` : `${name}.${member} = ${value};`);
|
|
150
|
+
pieces.slice(plainFirst ? 1 : 0).forEach((piece, i) => {
|
|
151
|
+
var _a;
|
|
152
|
+
if (piece.kind === 'break') {
|
|
153
|
+
this.line(`${name}.insertBreak('${piece.breakType}', 'End');`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const diff = (_a = diffs[i]) !== null && _a !== void 0 ? _a : [];
|
|
157
|
+
if (diff.length === 0) {
|
|
158
|
+
this.line(`${name}.insertText(${quote(piece.text)}, 'End');`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const range = `r${++this.ranges}`;
|
|
162
|
+
this.line(`const ${range} = ${name}.insertText(${quote(piece.text)}, 'End');`);
|
|
163
|
+
for (const [member, value] of diff)
|
|
164
|
+
this.line(`${range}.font.${member} = ${value};`);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The paragraph members that carry `w:pPr`; throws when it holds anything else.
|
|
169
|
+
*
|
|
170
|
+
* Reads the tree directly, not the property resolver: a script reproduces *markup*, not
|
|
171
|
+
* appearance, so it must set what the document states and nothing more. Emitting effective
|
|
172
|
+
* values would write a style's alignment, indents and size onto every paragraph and run of
|
|
173
|
+
* the generated document (CR-001 Phase B step 2; the same holds for `runFormat` below).
|
|
174
|
+
*/
|
|
175
|
+
paragraphProperties(p) {
|
|
176
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
177
|
+
const pPr = p.pPr;
|
|
178
|
+
if (!pPr)
|
|
179
|
+
return [];
|
|
180
|
+
if (!within(pPr, PPR_KEYS))
|
|
181
|
+
throw new Unexpressible(`paragraph properties (${keysOf(pPr).filter((k) => !PPR_KEYS.has(k)).join(', ')})`);
|
|
182
|
+
const out = [];
|
|
183
|
+
const style = (_a = pPr.pStyle) === null || _a === void 0 ? void 0 : _a.val;
|
|
184
|
+
if (style !== undefined && style !== 'Normal')
|
|
185
|
+
out.push(styleAssignment(style));
|
|
186
|
+
const jc = (_b = pPr.jc) === null || _b === void 0 ? void 0 : _b.val;
|
|
187
|
+
if (jc !== undefined) {
|
|
188
|
+
const alignment = jc === 'left' ? 'Left' : jc === 'center' ? 'Centered' : jc === 'right' ? 'Right' : jc === 'both' ? 'Justified' : undefined;
|
|
189
|
+
if (alignment === undefined)
|
|
190
|
+
throw new Unexpressible(`w:jc ${jc}`);
|
|
191
|
+
out.push(['alignment', quote(alignment)]);
|
|
192
|
+
}
|
|
193
|
+
const ind = pPr.ind;
|
|
194
|
+
if (ind) {
|
|
195
|
+
if (!within(ind, IND_KEYS))
|
|
196
|
+
throw new Unexpressible(`w:ind (${keysOf(ind).filter((k) => !IND_KEYS.has(k)).join(', ')})`);
|
|
197
|
+
if (ind.left !== undefined)
|
|
198
|
+
out.push(['leftIndent', num(ind.left / TWIPS_PER_POINT)]);
|
|
199
|
+
if (ind.right !== undefined)
|
|
200
|
+
out.push(['rightIndent', num(ind.right / TWIPS_PER_POINT)]);
|
|
201
|
+
if (ind.hanging !== undefined)
|
|
202
|
+
out.push(['firstLineIndent', num(-ind.hanging / TWIPS_PER_POINT)]);
|
|
203
|
+
else if (ind.firstLine !== undefined)
|
|
204
|
+
out.push(['firstLineIndent', num(ind.firstLine / TWIPS_PER_POINT)]);
|
|
205
|
+
}
|
|
206
|
+
const spacing = pPr.spacing;
|
|
207
|
+
if (spacing) {
|
|
208
|
+
if (!within(spacing, SPACING_KEYS))
|
|
209
|
+
throw new Unexpressible(`w:spacing (${keysOf(spacing).filter((k) => !SPACING_KEYS.has(k)).join(', ')})`);
|
|
210
|
+
if (spacing.before !== undefined)
|
|
211
|
+
out.push(['spaceBefore', num(spacing.before / TWIPS_PER_POINT)]);
|
|
212
|
+
if (spacing.after !== undefined)
|
|
213
|
+
out.push(['spaceAfter', num(spacing.after / TWIPS_PER_POINT)]);
|
|
214
|
+
if (spacing.line !== undefined) {
|
|
215
|
+
// Paragraph.lineSpacing writes w:lineRule="exact", so only an exact rule round-trips.
|
|
216
|
+
if (spacing.lineRule !== 'exact')
|
|
217
|
+
throw new Unexpressible(`w:spacing w:lineRule="${(_c = spacing.lineRule) !== null && _c !== void 0 ? _c : 'auto'}"`);
|
|
218
|
+
out.push(['lineSpacing', num(spacing.line / TWIPS_PER_POINT)]);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const outlineLvl = (_d = pPr.outlineLvl) === null || _d === void 0 ? void 0 : _d.val;
|
|
222
|
+
if (outlineLvl !== undefined)
|
|
223
|
+
out.push(['outlineLevel', num(outlineLvl + 1)]);
|
|
224
|
+
const numPr = pPr.numPr;
|
|
225
|
+
if (numPr) {
|
|
226
|
+
// A direct `w:numPr` is `attachToList(numId, ilvl)`, which names the `w:numId` as the
|
|
227
|
+
// style assignment above names the style id: both assume the target document defines it.
|
|
228
|
+
// `startNewList()` is not emitted - it is asynchronous and would need the numbering part
|
|
229
|
+
// to know whether the list is a bullet one (CR-002 section 17).
|
|
230
|
+
if (!within(numPr, NUMPR_KEYS))
|
|
231
|
+
throw new Unexpressible(`w:numPr (${keysOf(numPr).filter((k) => !NUMPR_KEYS.has(k)).join(', ')})`);
|
|
232
|
+
const numId = (_e = numPr.numId) === null || _e === void 0 ? void 0 : _e.val;
|
|
233
|
+
if (numId === undefined)
|
|
234
|
+
throw new Unexpressible('w:numPr with no w:numId');
|
|
235
|
+
out.push([CALL, numId === 0 ? 'detachFromList()' : `attachToList(${numId}, ${(_g = (_f = numPr.ilvl) === null || _f === void 0 ? void 0 : _f.val) !== null && _g !== void 0 ? _g : 0})`]);
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
/** A table through `insertTable` when the grid is plain text; otherwise `insertXml`. */
|
|
240
|
+
async table(element) {
|
|
241
|
+
const grid = tableGrid(element.value);
|
|
242
|
+
const hasInsertTable = typeof Body.prototype.insertTable === 'function';
|
|
243
|
+
if (!grid || !hasInsertTable) {
|
|
244
|
+
await this.fallback(element, grid ? 'insertTable is CR-002 phase C' : 'the table is not a plain grid of text');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const name = `t${++this.tables}`;
|
|
248
|
+
const values = `[${grid.values.map((row) => `[${row.map(quote).join(', ')}]`).join(', ')}]`;
|
|
249
|
+
this.line(`const ${name} = ${this.variable}.insertTable(${grid.rows}, ${grid.columns}, '${this.location}', ${values});`);
|
|
250
|
+
if (grid.style !== undefined) {
|
|
251
|
+
const [member, value] = styleAssignment(grid.style);
|
|
252
|
+
this.line(`${name}.${member} = ${value};`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** What no verb expresses: the marshalled fragment, which `insertXml` takes back. */
|
|
256
|
+
async fallback(element, reason) {
|
|
257
|
+
const xml = await marshalString(element);
|
|
258
|
+
if (reason !== undefined)
|
|
259
|
+
this.line(`// ${reason}: as XML`);
|
|
260
|
+
this.line(`await ${this.variable}.insertXml(${template(xml)}, '${this.location}');`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function sameFormat(a, b) {
|
|
264
|
+
return Object.keys(a).every((k) => a[k] === b[k]);
|
|
265
|
+
}
|
|
266
|
+
/** The text and breaks of a paragraph's runs, in order; throws for anything a verb cannot write. */
|
|
267
|
+
function piecesOf(p) {
|
|
268
|
+
var _a, _b, _c, _d, _e, _f;
|
|
269
|
+
const out = [];
|
|
270
|
+
for (const item of ((_a = p.content) !== null && _a !== void 0 ? _a : [])) {
|
|
271
|
+
const typeName = typeNameOf(item);
|
|
272
|
+
if (typeName !== undefined && IGNORED_TYPES.has(typeName))
|
|
273
|
+
continue;
|
|
274
|
+
if (typeName !== undefined && BOOKMARK_TYPES.has(typeName))
|
|
275
|
+
continue;
|
|
276
|
+
if (typeName !== 'org_docx4j_wml.R')
|
|
277
|
+
throw new Unexpressible(`${(_c = (_b = item.name) === null || _b === void 0 ? void 0 : _b.localPart) !== null && _c !== void 0 ? _c : 'content'} in the paragraph`);
|
|
278
|
+
const run = item.value;
|
|
279
|
+
if (run.rPr) {
|
|
280
|
+
if (!within(run.rPr, RPR_KEYS))
|
|
281
|
+
throw new Unexpressible(`run properties (${keysOf(run.rPr).filter((k) => !RPR_KEYS.has(k)).join(', ')})`);
|
|
282
|
+
if (run.rPr.rFonts && !within(run.rPr.rFonts, RFONTS_KEYS))
|
|
283
|
+
throw new Unexpressible('w:rFonts beyond ascii and hAnsi');
|
|
284
|
+
}
|
|
285
|
+
const format = readRunOptions(run.rPr);
|
|
286
|
+
for (const child of ((_d = run.content) !== null && _d !== void 0 ? _d : [])) {
|
|
287
|
+
const local = (_e = child.name) === null || _e === void 0 ? void 0 : _e.localPart;
|
|
288
|
+
if (local === 't') {
|
|
289
|
+
const text = String((_f = child.value.value) !== null && _f !== void 0 ? _f : '');
|
|
290
|
+
if (text !== '')
|
|
291
|
+
out.push({ kind: 'text', text, breakType: 'Line', format });
|
|
292
|
+
}
|
|
293
|
+
else if (local === 'tab') {
|
|
294
|
+
// insertText writes a tab character, not a w:tab; the text model reads both as \t.
|
|
295
|
+
out.push({ kind: 'text', text: '\t', breakType: 'Line', format });
|
|
296
|
+
}
|
|
297
|
+
else if (local === 'br') {
|
|
298
|
+
const type = child.value.type;
|
|
299
|
+
if (type !== undefined && type !== 'page' && type !== 'textWrapping')
|
|
300
|
+
throw new Unexpressible(`w:br type ${type}`);
|
|
301
|
+
out.push({ kind: 'break', text: '', breakType: type === 'page' ? 'Page' : 'Line', format });
|
|
302
|
+
}
|
|
303
|
+
else if (local === 'lastRenderedPageBreak') {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
throw new Unexpressible(`w:${local !== null && local !== void 0 ? local : '?'} in a run`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
/** `styleBuiltIn` with the `Word.Style` value for a built-in style id, else `style` with the display name. */
|
|
314
|
+
function styleAssignment(styleId) {
|
|
315
|
+
const builtIn = builtInOf(styleId);
|
|
316
|
+
return builtIn === 'Other' ? ['style', quote(displayNameOf(styleId))] : ['styleBuiltIn', quote(builtIn)];
|
|
317
|
+
}
|
|
318
|
+
/** A table as `insertTable` would build it, or undefined when it is more than a grid of text. */
|
|
319
|
+
function tableGrid(tbl) {
|
|
320
|
+
var _a, _b, _c, _d, _e;
|
|
321
|
+
const tblPr = tbl.tblPr;
|
|
322
|
+
if (tblPr && !within(tblPr, TBLPR_KEYS))
|
|
323
|
+
return undefined;
|
|
324
|
+
const values = [];
|
|
325
|
+
for (const item of ((_a = tbl.content) !== null && _a !== void 0 ? _a : [])) {
|
|
326
|
+
const typeName = typeNameOf(item);
|
|
327
|
+
if (typeName === 'org_docx4j_wml.CTTblGridCol' || typeName === 'org_docx4j_wml.TblGrid')
|
|
328
|
+
continue;
|
|
329
|
+
if (typeName !== 'org_docx4j_wml.Tr')
|
|
330
|
+
continue;
|
|
331
|
+
const row = [];
|
|
332
|
+
for (const cell of ((_b = item.value.content) !== null && _b !== void 0 ? _b : [])) {
|
|
333
|
+
if (typeNameOf(cell) !== 'org_docx4j_wml.Tc')
|
|
334
|
+
continue;
|
|
335
|
+
const tc = cell.value;
|
|
336
|
+
const blocks = ((_c = tc.content) !== null && _c !== void 0 ? _c : []).filter((b) => typeNameOf(b) !== 'org_docx4j_wml.TcPr');
|
|
337
|
+
if (blocks.length !== 1 || typeNameOf(blocks[0]) !== 'org_docx4j_wml.P')
|
|
338
|
+
return undefined;
|
|
339
|
+
const paragraph = blocks[0].value;
|
|
340
|
+
if (paragraph.pPr)
|
|
341
|
+
return undefined;
|
|
342
|
+
for (const run of ((_d = paragraph.content) !== null && _d !== void 0 ? _d : [])) {
|
|
343
|
+
if (typeNameOf(run) !== 'org_docx4j_wml.R')
|
|
344
|
+
return undefined;
|
|
345
|
+
if (run.value.rPr)
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
row.push(textOf(paragraph));
|
|
349
|
+
}
|
|
350
|
+
values.push(row);
|
|
351
|
+
}
|
|
352
|
+
if (values.length === 0)
|
|
353
|
+
return undefined;
|
|
354
|
+
const columns = values[0].length;
|
|
355
|
+
if (columns === 0 || values.some((r) => r.length !== columns))
|
|
356
|
+
return undefined;
|
|
357
|
+
const grid = { rows: values.length, columns, values };
|
|
358
|
+
const style = (_e = tblPr === null || tblPr === void 0 ? void 0 : tblPr.tblStyle) === null || _e === void 0 ? void 0 : _e.val;
|
|
359
|
+
if (style !== undefined)
|
|
360
|
+
grid.style = style;
|
|
361
|
+
return grid;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The content-API script that reproduces what it is given, as TypeScript against `body`: the verbs
|
|
365
|
+
* where they exist (`insertParagraph`, `style`, `alignment`, the indents and spacing, `insertText`
|
|
366
|
+
* with `Range.font` per run, `insertBreak`, `insertTable`), and `insertXml` with the marshalled
|
|
367
|
+
* fragment for the rest. Emitted for reveal codes, for teaching the API by example, and for an
|
|
368
|
+
* agent that wants the calls rather than the tree.
|
|
369
|
+
*
|
|
370
|
+
* The script is a sequence of statements over `body`; it uses `await` for the `insertXml` lines,
|
|
371
|
+
* so run it in an async function (`new Function('body', 'return (async () => {' + script + '})()')`).
|
|
372
|
+
*/
|
|
373
|
+
export async function toApiScript(target, options = {}) {
|
|
374
|
+
const emitter = new Emitter(options);
|
|
375
|
+
if (target instanceof Paragraph)
|
|
376
|
+
await emitter.paragraph(target.element);
|
|
377
|
+
else if (target instanceof Range)
|
|
378
|
+
await emitter.range(target);
|
|
379
|
+
else if (target instanceof Body) {
|
|
380
|
+
for (const element of target.content)
|
|
381
|
+
await emitter.element(element);
|
|
382
|
+
}
|
|
383
|
+
else if (Array.isArray(target)) {
|
|
384
|
+
for (const element of target)
|
|
385
|
+
await emitter.element(element);
|
|
386
|
+
}
|
|
387
|
+
else if (isElement(target)) {
|
|
388
|
+
await emitter.element(target);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
throw new Docx4JException('toApiScript takes a Body, a Paragraph, a Range, or w:p / w:tbl elements');
|
|
392
|
+
}
|
|
393
|
+
return emitter.script();
|
|
394
|
+
}
|
|
@@ -28,6 +28,7 @@ export declare const ContentTypes: {
|
|
|
28
28
|
readonly WORDPROCESSINGML_COMMENTS: "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
|
|
29
29
|
readonly WORDPROCESSINGML_COMMENTS_EXTENDED: "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml";
|
|
30
30
|
readonly WORDPROCESSINGML_COMMENTS_IDS: "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml";
|
|
31
|
+
readonly WORDPROCESSINGML_COMMENTS_EXTENSIBLE: "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml";
|
|
31
32
|
readonly WORDPROCESSINGML_ENDNOTES: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml";
|
|
32
33
|
readonly WORDPROCESSINGML_FONTTABLE: "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml";
|
|
33
34
|
readonly WORDPROCESSINGML_FOOTER: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml";
|
|
@@ -91,6 +92,16 @@ export declare const ContentTypes: {
|
|
|
91
92
|
readonly SPREADSHEETML_COMMENTS: "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml";
|
|
92
93
|
readonly SPREADSHEETML_CONNECTIONS: "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml";
|
|
93
94
|
readonly SPREADSHEETML_EXTERNAL_LINK: "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml";
|
|
95
|
+
readonly SPREADSHEETML_SLICER_CACHE: "application/vnd.ms-excel.slicerCache+xml";
|
|
96
|
+
readonly SPREADSHEETML_SLICERS: "application/vnd.ms-excel.slicer+xml";
|
|
97
|
+
readonly SPREADSHEETML_TIMELINE_CACHE: "application/vnd.ms-excel.timelineCache+xml";
|
|
98
|
+
readonly SPREADSHEETML_TIMELINES: "application/vnd.ms-excel.timeline+xml";
|
|
99
|
+
readonly SPREADSHEETML_CONTROL_PROPERTIES: "application/vnd.ms-excel.controlproperties+xml";
|
|
100
|
+
readonly SPREADSHEETML_CUSTOM_DATA_PROPERTIES: "application/vnd.openxmlformats-officedocument.customDataProperties+xml";
|
|
101
|
+
/** Generic, so `CustomDataPart` is registered by relationship type instead. */
|
|
102
|
+
readonly SPREADSHEETML_CUSTOM_DATA: "application/binary";
|
|
103
|
+
readonly SPREADSHEETML_SURVEY: "application/vnd.ms-excel.Survey+xml";
|
|
104
|
+
readonly SPREADSHEETML_DATA_MODEL: "application/vnd.openxmlformats-officedocument.model+data";
|
|
94
105
|
readonly DIGITAL_SIGNATURE_ORIGIN_PART: "application/vnd.openxmlformats-package.digital-signature-origin";
|
|
95
106
|
readonly DIGITAL_SIGNATURE_XML_SIGNATURE_PART: "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml";
|
|
96
107
|
readonly INK_ML: "application/inkml+xml";
|
|
@@ -28,6 +28,7 @@ export const ContentTypes = {
|
|
|
28
28
|
WORDPROCESSINGML_COMMENTS: 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml',
|
|
29
29
|
WORDPROCESSINGML_COMMENTS_EXTENDED: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml',
|
|
30
30
|
WORDPROCESSINGML_COMMENTS_IDS: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml',
|
|
31
|
+
WORDPROCESSINGML_COMMENTS_EXTENSIBLE: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml',
|
|
31
32
|
WORDPROCESSINGML_ENDNOTES: 'application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml',
|
|
32
33
|
WORDPROCESSINGML_FONTTABLE: 'application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml',
|
|
33
34
|
WORDPROCESSINGML_FOOTER: 'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml',
|
|
@@ -91,6 +92,19 @@ export const ContentTypes = {
|
|
|
91
92
|
SPREADSHEETML_COMMENTS: 'application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml',
|
|
92
93
|
SPREADSHEETML_CONNECTIONS: 'application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml',
|
|
93
94
|
SPREADSHEETML_EXTERNAL_LINK: 'application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml',
|
|
95
|
+
// The Excel 2010 and 2013 extension parts ([MS-XLSX] 2.1; docx4j 17.2.0 CR-022, CR-004 here).
|
|
96
|
+
// The strings are Excel 365's as docx4j measured them: the timeline pair is lowercase, and
|
|
97
|
+
// the survey's capital S is Excel's too.
|
|
98
|
+
SPREADSHEETML_SLICER_CACHE: 'application/vnd.ms-excel.slicerCache+xml',
|
|
99
|
+
SPREADSHEETML_SLICERS: 'application/vnd.ms-excel.slicer+xml',
|
|
100
|
+
SPREADSHEETML_TIMELINE_CACHE: 'application/vnd.ms-excel.timelineCache+xml',
|
|
101
|
+
SPREADSHEETML_TIMELINES: 'application/vnd.ms-excel.timeline+xml',
|
|
102
|
+
SPREADSHEETML_CONTROL_PROPERTIES: 'application/vnd.ms-excel.controlproperties+xml',
|
|
103
|
+
SPREADSHEETML_CUSTOM_DATA_PROPERTIES: 'application/vnd.openxmlformats-officedocument.customDataProperties+xml',
|
|
104
|
+
/** Generic, so `CustomDataPart` is registered by relationship type instead. */
|
|
105
|
+
SPREADSHEETML_CUSTOM_DATA: 'application/binary',
|
|
106
|
+
SPREADSHEETML_SURVEY: 'application/vnd.ms-excel.Survey+xml',
|
|
107
|
+
SPREADSHEETML_DATA_MODEL: 'application/vnd.openxmlformats-officedocument.model+data',
|
|
94
108
|
DIGITAL_SIGNATURE_ORIGIN_PART: 'application/vnd.openxmlformats-package.digital-signature-origin',
|
|
95
109
|
DIGITAL_SIGNATURE_XML_SIGNATURE_PART: 'application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml',
|
|
96
110
|
INK_ML: 'application/inkml+xml',
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PartStore, PartSink, PutOptions } from './PartStore.mjs';
|
|
2
|
+
/**
|
|
3
|
+
* A package spread over a directory: `word/document.xml` is the file `<dir>/word/document.xml`,
|
|
4
|
+
* `[Content_Types].xml` is read from the directory like any other entry, and a part's size comes
|
|
5
|
+
* from the file rather than from a central directory.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const pkg = await WordprocessingMLPackage.load(await DirectoryPartStore.open('unzipped'));
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* The directory is scanned once (`open`), because a `PartStore` lists its names synchronously.
|
|
12
|
+
* A part that is never touched is copied from here to the sink byte for byte, as from a zip, so
|
|
13
|
+
* zip to directory to zip reproduces every part.
|
|
14
|
+
*/
|
|
15
|
+
export declare class DirectoryPartStore implements PartStore {
|
|
16
|
+
readonly dir: string;
|
|
17
|
+
/** Store name (no leading '/') to [absolute file path, size]. */
|
|
18
|
+
private readonly entries;
|
|
19
|
+
private constructor();
|
|
20
|
+
/** Scans a directory and returns a store over it. */
|
|
21
|
+
static open(dir: string): Promise<DirectoryPartStore>;
|
|
22
|
+
private scan;
|
|
23
|
+
partNames(): Iterable<string>;
|
|
24
|
+
has(partName: string): boolean;
|
|
25
|
+
load(partName: string): Promise<Uint8Array>;
|
|
26
|
+
size(partName: string): number | undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Writes a package to a directory (docx4j `UnzippedPartStore`'s save half): one file per part,
|
|
30
|
+
* directories created as needed, `[Content_Types].xml` first. `finish()` resolves to the
|
|
31
|
+
* directory path.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* await pkg.saveTo(new DirectoryPartSink('unzipped'));
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Nothing already in the directory is removed: a part the package no longer has stays on disk.
|
|
38
|
+
*/
|
|
39
|
+
export declare class DirectoryPartSink implements PartSink<string> {
|
|
40
|
+
readonly dir: string;
|
|
41
|
+
/** Insertion-ordered, so `[Content_Types].xml` (which `savePackage` puts first) is written first. */
|
|
42
|
+
private readonly pending;
|
|
43
|
+
constructor(dir: string);
|
|
44
|
+
put(partName: string, bytes: Uint8Array, _options?: PutOptions): void;
|
|
45
|
+
finish(): Promise<string>;
|
|
46
|
+
private write;
|
|
47
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// An unzipped package on the file system (docx4j io3.stores.UnzippedPartStore). Node only: it is
|
|
2
|
+
// the one module here that imports `node:` builtins, and it is exported from the `./node` subpath
|
|
3
|
+
// only, never from `.` or `./opc`, so a browser or add-in bundle never sees them.
|
|
4
|
+
import { readFile, readdir, stat, mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { join, dirname, sep } from 'node:path';
|
|
6
|
+
import { CONTENT_TYPES_NAME } from './PartName.mjs';
|
|
7
|
+
import { Docx4JException } from './exceptions.mjs';
|
|
8
|
+
/**
|
|
9
|
+
* A package spread over a directory: `word/document.xml` is the file `<dir>/word/document.xml`,
|
|
10
|
+
* `[Content_Types].xml` is read from the directory like any other entry, and a part's size comes
|
|
11
|
+
* from the file rather than from a central directory.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const pkg = await WordprocessingMLPackage.load(await DirectoryPartStore.open('unzipped'));
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* The directory is scanned once (`open`), because a `PartStore` lists its names synchronously.
|
|
18
|
+
* A part that is never touched is copied from here to the sink byte for byte, as from a zip, so
|
|
19
|
+
* zip to directory to zip reproduces every part.
|
|
20
|
+
*/
|
|
21
|
+
export class DirectoryPartStore {
|
|
22
|
+
constructor(dir) {
|
|
23
|
+
this.dir = dir;
|
|
24
|
+
/** Store name (no leading '/') to [absolute file path, size]. */
|
|
25
|
+
this.entries = new Map();
|
|
26
|
+
}
|
|
27
|
+
/** Scans a directory and returns a store over it. */
|
|
28
|
+
static async open(dir) {
|
|
29
|
+
const store = new DirectoryPartStore(dir);
|
|
30
|
+
await store.scan('');
|
|
31
|
+
return store;
|
|
32
|
+
}
|
|
33
|
+
async scan(relative) {
|
|
34
|
+
const here = relative === '' ? this.dir : join(this.dir, relative);
|
|
35
|
+
let listing;
|
|
36
|
+
try {
|
|
37
|
+
listing = await readdir(here, { withFileTypes: true });
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
throw new Docx4JException(`Cannot read package directory ${here}`, { cause: e });
|
|
41
|
+
}
|
|
42
|
+
// Names in a stable order, with [Content_Types].xml first as a zip written by Word has it.
|
|
43
|
+
const names = listing.map((entry) => entry.name).sort(compareNames);
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
const childRelative = relative === '' ? name : `${relative}/${name}`;
|
|
46
|
+
const childPath = join(this.dir, childRelative);
|
|
47
|
+
const info = await stat(childPath);
|
|
48
|
+
if (info.isDirectory())
|
|
49
|
+
await this.scan(childRelative);
|
|
50
|
+
else if (info.isFile())
|
|
51
|
+
this.entries.set(childRelative, [childPath, info.size]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
partNames() {
|
|
55
|
+
return this.entries.keys();
|
|
56
|
+
}
|
|
57
|
+
has(partName) {
|
|
58
|
+
return this.entries.has(normalize(partName));
|
|
59
|
+
}
|
|
60
|
+
async load(partName) {
|
|
61
|
+
const entry = this.entries.get(normalize(partName));
|
|
62
|
+
if (!entry)
|
|
63
|
+
throw new Docx4JException(`No part ${partName} in ${this.dir}`);
|
|
64
|
+
return new Uint8Array(await readFile(entry[0]));
|
|
65
|
+
}
|
|
66
|
+
size(partName) {
|
|
67
|
+
var _a;
|
|
68
|
+
return (_a = this.entries.get(normalize(partName))) === null || _a === void 0 ? void 0 : _a[1];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Writes a package to a directory (docx4j `UnzippedPartStore`'s save half): one file per part,
|
|
73
|
+
* directories created as needed, `[Content_Types].xml` first. `finish()` resolves to the
|
|
74
|
+
* directory path.
|
|
75
|
+
*
|
|
76
|
+
* ```ts
|
|
77
|
+
* await pkg.saveTo(new DirectoryPartSink('unzipped'));
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* Nothing already in the directory is removed: a part the package no longer has stays on disk.
|
|
81
|
+
*/
|
|
82
|
+
export class DirectoryPartSink {
|
|
83
|
+
constructor(dir) {
|
|
84
|
+
this.dir = dir;
|
|
85
|
+
/** Insertion-ordered, so `[Content_Types].xml` (which `savePackage` puts first) is written first. */
|
|
86
|
+
this.pending = new Map();
|
|
87
|
+
}
|
|
88
|
+
put(partName, bytes, _options) {
|
|
89
|
+
this.pending.set(normalize(partName), bytes);
|
|
90
|
+
}
|
|
91
|
+
async finish() {
|
|
92
|
+
const first = this.pending.get(CONTENT_TYPES_NAME);
|
|
93
|
+
if (first !== undefined) {
|
|
94
|
+
// written first even if a caller put it later
|
|
95
|
+
this.pending.delete(CONTENT_TYPES_NAME);
|
|
96
|
+
await this.write(CONTENT_TYPES_NAME, first);
|
|
97
|
+
}
|
|
98
|
+
for (const [name, bytes] of this.pending)
|
|
99
|
+
await this.write(name, bytes);
|
|
100
|
+
this.pending.clear();
|
|
101
|
+
return this.dir;
|
|
102
|
+
}
|
|
103
|
+
async write(partName, bytes) {
|
|
104
|
+
const path = join(this.dir, partName);
|
|
105
|
+
// A part name that climbs out of the directory (docx4j's "Zip Slip" check).
|
|
106
|
+
if (path !== this.dir && !path.startsWith(this.dir.endsWith(sep) ? this.dir : this.dir + sep)) {
|
|
107
|
+
throw new Docx4JException(`Part ${partName} is outside the target directory`);
|
|
108
|
+
}
|
|
109
|
+
await mkdir(dirname(path), { recursive: true });
|
|
110
|
+
await writeFile(path, bytes);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function normalize(name) {
|
|
114
|
+
return name.startsWith('/') ? name.substring(1) : name;
|
|
115
|
+
}
|
|
116
|
+
/** `[Content_Types].xml` first, then by name; the order the entries are listed in. */
|
|
117
|
+
function compareNames(a, b) {
|
|
118
|
+
if (a === CONTENT_TYPES_NAME)
|
|
119
|
+
return -1;
|
|
120
|
+
if (b === CONTENT_TYPES_NAME)
|
|
121
|
+
return 1;
|
|
122
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
123
|
+
}
|