@docx4j/core-ts 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +3 -1
  2. package/dist/html/convert.d.mts +116 -0
  3. package/dist/html/convert.mjs +541 -0
  4. package/dist/html/elements.d.mts +19 -0
  5. package/dist/html/elements.mjs +182 -0
  6. package/dist/html/index.d.mts +2 -0
  7. package/dist/html/index.mjs +4 -0
  8. package/dist/model/content/Body.mjs +7 -0
  9. package/dist/model/content/InlinePicture.d.mts +18 -3
  10. package/dist/model/content/InlinePicture.mjs +44 -7
  11. package/dist/model/content/Table.d.mts +38 -0
  12. package/dist/model/content/Table.mjs +80 -2
  13. package/dist/model/content/index.d.mts +2 -1
  14. package/dist/model/content/index.mjs +2 -1
  15. package/dist/model/content/stylesFacade.d.mts +4 -2
  16. package/dist/model/content/stylesFacade.mjs +20 -6
  17. package/dist/model/content/tableColumns.d.mts +48 -0
  18. package/dist/model/content/tableColumns.mjs +484 -0
  19. package/dist/model/customxml/CustomXmlPartCollection.d.mts +6 -1
  20. package/dist/model/customxml/CustomXmlPartCollection.mjs +19 -3
  21. package/dist/model/customxml/bindings.d.mts +5 -2
  22. package/dist/model/customxml/bindings.mjs +21 -6
  23. package/dist/model/customxml/index.d.mts +3 -0
  24. package/dist/model/customxml/index.mjs +3 -0
  25. package/dist/model/customxml/opendope.d.mts +32 -0
  26. package/dist/model/customxml/opendope.mjs +56 -0
  27. package/dist/model/customxml/pictures.d.mts +54 -0
  28. package/dist/model/customxml/pictures.mjs +186 -0
  29. package/dist/model/customxml/xhtml.d.mts +42 -0
  30. package/dist/model/customxml/xhtml.mjs +162 -0
  31. package/dist/packages/WordprocessingMLPackage.mjs +1 -0
  32. package/dist/parts/wml/knownStyles.d.mts +1 -0
  33. package/dist/parts/wml/knownStyles.mjs +2233 -0
  34. package/package.json +6 -1
package/README.md CHANGED
@@ -46,7 +46,9 @@ A part that is never touched is written back byte for byte; `getContents()` (or
46
46
  it) marks a part for re-marshalling. `mc:AlternateContent` is resolved when a part is
47
47
  unmarshalled, as Word does on open (`{ mcePreprocess: false }` to keep it). Subpaths
48
48
  `@docx4j/core-ts/opc`, `/parts`, `/packages` and `/model` give the layers separately,
49
- `/office-js` the `Word` shim (below), and `/node` the one Node-only piece (the unzipped-directory
49
+ `/office-js` the `Word` shim (below), `/html` HTML to WordprocessingML (a converter to an
50
+ intermediate form and a builder from it, CR-005 section 8; the HTML parser is yours, a browser's
51
+ `DOMParser` by default), and `/node` the one Node-only piece (the unzipped-directory
50
52
  container; nothing else in the package imports a `node:` builtin, so a browser or add-in bundle
51
53
  never sees one).
52
54
 
@@ -0,0 +1,116 @@
1
+ /** The '#RRGGBB' of one of Word's sixteen highlight names, or undefined for anything else (`mso-highlight` is read through it). */
2
+ export declare function highlightHexOf(name: string): string | undefined;
3
+ /** The highlight name for one of the sixteen colours, by its '#RRGGBB'. */
4
+ export declare function highlightNameOf(hex: string): string | undefined;
5
+ /** A web or mail address a link may keep (`http`, `https`, `mailto`), trimmed; anything else undefined. */
6
+ export declare function webAddress(url: string): string | undefined;
7
+ export interface PasteRun {
8
+ text: string;
9
+ bold?: true;
10
+ italic?: true;
11
+ underline?: true;
12
+ strike?: true;
13
+ vertAlign?: 'superscript' | 'subscript';
14
+ /** The first family of `font-family`, when it is a name and not a generic. */
15
+ fontName?: string;
16
+ /** Points. */
17
+ fontSize?: number;
18
+ /** '#RRGGBB', or 'auto' for `windowtext`. */
19
+ color?: string;
20
+ /** '#RRGGBB' of one of Word's highlight names (`mso-highlight`, else a `background` naming one). */
21
+ highlight?: string;
22
+ /** A character style id of the document (Markdown's inline code, `HTMLCode`). */
23
+ style?: string;
24
+ /** A link's address (Markdown's links): a relationship made before the transaction, the text in Word's look. */
25
+ link?: string;
26
+ }
27
+ export type PasteInline = PasteRun | {
28
+ br: true;
29
+ } | {
30
+ tab: true;
31
+ };
32
+ export interface PasteList {
33
+ /** Word's `lfoN`, or the list element's own key: one definition per key. */
34
+ key: string;
35
+ level: number;
36
+ bullet: boolean;
37
+ }
38
+ export interface PasteParagraph {
39
+ kind: 'paragraph';
40
+ /** A style id of the document, or null for the default. */
41
+ style: string | null;
42
+ list?: PasteList;
43
+ /** `w:jc` from `text-align` or the `align` attribute. */
44
+ jc?: string;
45
+ /** Twips from `margin-left`, `margin-right` and `text-indent` (a negative one a hanging indent). */
46
+ ind?: {
47
+ start?: number;
48
+ end?: number;
49
+ firstLine?: number;
50
+ hanging?: number;
51
+ };
52
+ /** Twips from `margin-top`; `margin-bottom` and `line-height` are dropped and counted. */
53
+ spacing?: {
54
+ before: number;
55
+ };
56
+ inlines: PasteInline[];
57
+ }
58
+ export interface PasteCell {
59
+ blocks: PasteBlock[];
60
+ colspan: number;
61
+ rowspan: number;
62
+ /** Twips, when the cell said. */
63
+ width?: number;
64
+ }
65
+ export interface PasteTable {
66
+ kind: 'table';
67
+ rows: PasteCell[][];
68
+ /** The document's table grid style, when it has one. */
69
+ style: string | null;
70
+ /** The first row repeated at the top of each page (`w:tblHeader`): a Markdown table's header row. */
71
+ headerRow?: boolean;
72
+ }
73
+ export type PasteBlock = PasteParagraph | PasteTable;
74
+ export interface PasteReport {
75
+ paragraphs: number;
76
+ lists: number;
77
+ tables: number;
78
+ /** What was dropped, by kind, with how many times. */
79
+ dropped: Record<string, number>;
80
+ /** Built-in styles the paste names that the document lacked: a caller splices them in from the defaults (`styles.ensure`) before building, else the paragraphs stay plain. */
81
+ stylesWanted: string[];
82
+ }
83
+ export interface PasteResult {
84
+ blocks: PasteBlock[];
85
+ report: PasteReport;
86
+ }
87
+ /** What the converter asks of the document: its styles, by id and by name. */
88
+ export interface StyleLookup {
89
+ /** The style id for a name (`heading 1`, `Title`), case-insensitively, or undefined. */
90
+ idFor(name: string): string | undefined;
91
+ hasId(id: string): boolean;
92
+ }
93
+ /** The lookup over the document's paragraph and character styles, plus the table style ids the caller found (the styles list carries no table styles). */
94
+ /** A style's id and name, as a document's styles part lists them. */
95
+ export interface StyleNaming {
96
+ id: string;
97
+ name: string;
98
+ }
99
+ export declare function styleLookupOf(entries: readonly StyleNaming[], tableStyleIds?: readonly string[]): StyleLookup;
100
+ /** Word's `CF_HTML` header (`Version:`, `StartHTML:` and the rest) precedes `<html` in a saved capture; a browser hands the markup alone. */
101
+ export declare function clipboardMarkup(text: string): string;
102
+ /**
103
+ * True when the HTML is a terminal's screen (ED-005 section 12.67): inline content only, every
104
+ * piece of text in a monospace font, as Konsole's "Copy text as HTML" writes it (a span in
105
+ * `font-family:monospace` holding a coloured span per run and a `<br>` per line,
106
+ * `fixtures/paste/terminal/`). Its colours and font are the terminal's theme, not content, and the
107
+ * converter would make a paragraph of each span, so the paste takes the plain text instead.
108
+ */
109
+ export declare function isTerminalHtml(html: string, options?: {
110
+ parser?: (html: string) => Document;
111
+ }): boolean;
112
+ /** Convert clipboard HTML to the intermediate form, keeping what the outline names and counting the rest. */
113
+ export declare function convertHtml(html: string, lookup: StyleLookup, options?: {
114
+ parser?: (html: string) => Document;
115
+ }): PasteResult;
116
+ /** The list definitions a paste needs: a `w:numId` per list key. */
@@ -0,0 +1,541 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // HTML to an intermediate form (CR-005 section 8; moved from docx4j-ts-editor's `view/paste.ts` on
4
+ // 2026-10-08 by its copyright owner's decision, docx4j-ts-editor ED-001 decision 26, relicensed from
5
+ // GPL-3.0-or-later; the editor imports it back). Written for Word's clipboard HTML (the editor's
6
+ // ED-003 section 8.3): `<h1>` to `<h6>` for the heading styles, `<p class=MsoNormal>` and `MsoTitle`,
7
+ // `<b>`, `<i>`, `<u>`, `<s>`, `<sup>`, `<sub>` and their `span` style forms, `<br>`, a tab as a span
8
+ // with `mso-tab-count`, lists as `<ul>` and `<ol>` or as `MsoListParagraph` paragraphs with
9
+ // `mso-list`, tables as `<table class=MsoTableGrid>`, and a great deal else; it serves any XHTML a
10
+ // data node holds for an OpenDoPE binding (the specification's section 9) the same way. The
11
+ // converter keeps what a document's styles name and the direct formatting an element states
12
+ // (alignment, indents, space before, the font, size, colour and highlight of a run, a web or mail
13
+ // link), drops the rest and counts it, so a caller can say what was dropped. `margin-bottom` and
14
+ // `line-height` stay dropped: Word writes them on every paragraph from the source's style. Pure over
15
+ // its inputs: a DOM walk to the intermediate form, which `elements.mts` turns into
16
+ // WordprocessingML. The HTML parser is the caller's: a browser's `DOMParser` by default, jsdom's or
17
+ // linkedom's in Node.
18
+ /** Word's sixteen highlight names and their colours. */
19
+ const HIGHLIGHT = {
20
+ black: '#000000', blue: '#0000FF', cyan: '#00FFFF', green: '#008000', magenta: '#FF00FF', red: '#FF0000', yellow: '#FFFF00',
21
+ white: '#FFFFFF', darkBlue: '#00008B', darkCyan: '#008B8B', darkGreen: '#006400', darkMagenta: '#8B008B', darkRed: '#8B0000',
22
+ darkYellow: '#FFD700', darkGray: '#A9A9A9', lightGray: '#D3D3D3',
23
+ };
24
+ /** The '#RRGGBB' of one of Word's sixteen highlight names, or undefined for anything else (`mso-highlight` is read through it). */
25
+ export function highlightHexOf(name) {
26
+ return HIGHLIGHT[name];
27
+ }
28
+ /** The highlight name for one of the sixteen colours, by its '#RRGGBB'. */
29
+ export function highlightNameOf(hex) {
30
+ const upper = hex.toUpperCase();
31
+ for (const [name, value] of Object.entries(HIGHLIGHT))
32
+ if (value === upper)
33
+ return name;
34
+ return undefined;
35
+ }
36
+ /** A web or mail address a link may keep (`http`, `https`, `mailto`), trimmed; anything else undefined. */
37
+ export function webAddress(url) {
38
+ var _a, _b;
39
+ const scheme = (_b = (_a = /^([a-z][a-z0-9+.-]*):/i.exec(url.trim())) === null || _a === void 0 ? void 0 : _a[1]) === null || _b === void 0 ? void 0 : _b.toLowerCase();
40
+ return scheme === 'http' || scheme === 'https' || scheme === 'mailto' ? url.trim() : undefined;
41
+ }
42
+ export function styleLookupOf(entries, tableStyleIds = []) {
43
+ const byName = new Map(entries.map((e) => [e.name.toLowerCase(), e.id]));
44
+ const ids = new Set([...entries.map((e) => e.id), ...tableStyleIds]);
45
+ return { idFor: (name) => byName.get(name.toLowerCase()), hasId: (id) => ids.has(id) };
46
+ }
47
+ // ---------------------------------------------------------------- the clipboard text
48
+ /** Word's `CF_HTML` header (`Version:`, `StartHTML:` and the rest) precedes `<html` in a saved capture; a browser hands the markup alone. */
49
+ export function clipboardMarkup(text) {
50
+ // Word (and a browser handing on Word's clipboard) marks the selection: what is inside the
51
+ // markers is the paste; the head's styles, the body tag and a capture's tail are not.
52
+ const s = text.indexOf('<!--StartFragment-->');
53
+ const e = text.indexOf('<!--EndFragment-->');
54
+ if (s >= 0 && e > s)
55
+ return text.slice(s + '<!--StartFragment-->'.length, e);
56
+ const at = text.search(/<html[\s>]/i);
57
+ if (at > 0 && text.slice(0, at).includes('StartHTML:'))
58
+ return text.slice(at);
59
+ return text.replace(/^\uFEFF/, '');
60
+ }
61
+ /** Elements that make a block of their own: HTML with any of them is a document's, not a screen's. */
62
+ const BLOCK_ELEMENTS = 'p,div,h1,h2,h3,h4,h5,h6,ul,ol,li,dl,table,pre,blockquote,hr,section,article,header,footer,main,nav,aside,figure,img';
63
+ /** A font-family a terminal or a code view would name. */
64
+ const MONOSPACE = /mono|courier|consolas|menlo|monaco/i;
65
+ /**
66
+ * True when the HTML is a terminal's screen (ED-005 section 12.67): inline content only, every
67
+ * piece of text in a monospace font, as Konsole's "Copy text as HTML" writes it (a span in
68
+ * `font-family:monospace` holding a coloured span per run and a `<br>` per line,
69
+ * `fixtures/paste/terminal/`). Its colours and font are the terminal's theme, not content, and the
70
+ * converter would make a paragraph of each span, so the paste takes the plain text instead.
71
+ */
72
+ export function isTerminalHtml(html, options = {}) {
73
+ var _a, _b;
74
+ const parse = (_a = options.parser) !== null && _a !== void 0 ? _a : ((h) => new DOMParser().parseFromString(h, 'text/html'));
75
+ const doc = parse(clipboardMarkup(html));
76
+ const body = rootOf(doc);
77
+ if (!body || body.querySelector(BLOCK_ELEMENTS))
78
+ return false;
79
+ const monospaced = (node) => {
80
+ var _a;
81
+ for (let el = node.parentElement; el && el !== body; el = el.parentElement) {
82
+ if (MONOSPACE.test((_a = styleOf(el).get('font-family')) !== null && _a !== void 0 ? _a : '') || /^(code|kbd|samp|tt)$/i.test(el.tagName))
83
+ return true;
84
+ }
85
+ return false;
86
+ };
87
+ let text = false;
88
+ const walker = doc.createTreeWalker(body, 4 /* NodeFilter.SHOW_TEXT */);
89
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
90
+ if (!/\S/.test((_b = node.textContent) !== null && _b !== void 0 ? _b : ''))
91
+ continue;
92
+ if (!monospaced(node))
93
+ return false;
94
+ text = true;
95
+ }
96
+ return text;
97
+ }
98
+ // ---------------------------------------------------------------- the walk
99
+ const HEADING = /^h([1-6])$/i;
100
+ const BULLET_GLYPHS = /^[•·●◦▪■–—o\-∙‣⁃]+$/;
101
+ /** A CSS length as twips: points, centimetres, millimetres, inches and pixels (15 twips each, as the schema counts), else undefined. */
102
+ function twipsOf(value) {
103
+ if (value === undefined)
104
+ return undefined;
105
+ const m = /^(-?\d*\.?\d+)\s*(pt|cm|mm|in|px)?$/.exec(value.trim());
106
+ if (!m)
107
+ return undefined;
108
+ const n = parseFloat(m[1]);
109
+ const per = { pt: 20, cm: 566.93, mm: 56.693, in: 1440, px: 15 };
110
+ if (n === 0)
111
+ return 0;
112
+ const unit = m[2];
113
+ if (!unit)
114
+ return undefined;
115
+ return Math.round(n * per[unit]);
116
+ }
117
+ /** The CSS colour names Word writes instead of a hex value (the basic sixteen and orange), and `windowtext` as the automatic colour. */
118
+ const COLOUR_NAMES = {
119
+ black: '#000000', silver: '#C0C0C0', gray: '#808080', grey: '#808080', white: '#FFFFFF', maroon: '#800000', red: '#FF0000', purple: '#800080', fuchsia: '#FF00FF',
120
+ green: '#008000', lime: '#00FF00', olive: '#808000', yellow: '#FFFF00', navy: '#000080', blue: '#0000FF', teal: '#008080', aqua: '#00FFFF', orange: '#FFA500',
121
+ windowtext: 'auto',
122
+ };
123
+ /** A CSS colour as '#RRGGBB' (or 'auto'), else undefined. */
124
+ function colourOf(value) {
125
+ if (value === undefined)
126
+ return undefined;
127
+ const v = value.trim();
128
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(v);
129
+ if (hex) {
130
+ const h = hex[1];
131
+ return `#${(h.length === 3 ? h.split('').map((c) => c + c).join('') : h).toUpperCase()}`;
132
+ }
133
+ return COLOUR_NAMES[v.toLowerCase()];
134
+ }
135
+ /** The first family of a `font-family` list, unquoted, unless it is a generic family. */
136
+ function fontNameOf(value) {
137
+ if (value === undefined)
138
+ return undefined;
139
+ const first = value.split(',')[0].trim().replace(/^["']|["']$/g, '').trim();
140
+ if (first === '' || /^(serif|sans-serif|monospace|cursive|fantasy|system-ui|inherit|initial)$/i.test(first))
141
+ return undefined;
142
+ return first;
143
+ }
144
+ /** A `font-size` in points (pixels at three quarters of a point), else undefined. */
145
+ function fontSizeOf(value) {
146
+ if (value === undefined)
147
+ return undefined;
148
+ const m = /^(\d*\.?\d+)\s*(pt|px)$/.exec(value.trim());
149
+ if (!m)
150
+ return undefined;
151
+ const n = parseFloat(m[1]);
152
+ const pt = m[2] === 'px' ? n * 0.75 : n;
153
+ return pt > 0 ? Math.round(pt * 2) / 2 : undefined;
154
+ }
155
+ /** `w:jc` for a `text-align` value or an `align` attribute. */
156
+ function jcOf(value) {
157
+ switch ((value !== null && value !== void 0 ? value : '').trim().toLowerCase()) {
158
+ case 'left':
159
+ case 'start': return 'left';
160
+ case 'center': return 'center';
161
+ case 'right':
162
+ case 'end': return 'right';
163
+ case 'justify': return 'both';
164
+ default: return undefined;
165
+ }
166
+ }
167
+ function styleOf(el) {
168
+ const out = new Map();
169
+ const raw = el.getAttribute('style');
170
+ if (!raw)
171
+ return out;
172
+ for (const part of raw.split(';')) {
173
+ const i = part.indexOf(':');
174
+ if (i < 0)
175
+ continue;
176
+ out.set(part.slice(0, i).trim().toLowerCase(), part.slice(i + 1).trim().replace(/\s+/g, ' '));
177
+ }
178
+ return out;
179
+ }
180
+ /** Word's built-in styles a paste may name that a document can lack: the id docx4j's defaults define them under. */
181
+ const BUILT_IN = { Title: 'Title', Subtitle: 'Subtitle', Quote: 'Quote', IntenseQuote: 'IntenseQuote', ListParagraph: 'ListParagraph', Caption: 'Caption' };
182
+ /** The style a Word paragraph names: `<hN>`, `MsoTitle`, `MsoSubtitle`, a class the document has a style for, else the default; a built-in the document lacks is wanted. */
183
+ function paragraphStyle(el, w) {
184
+ var _a, _b, _c, _d;
185
+ const m = HEADING.exec(el.tagName);
186
+ if (m) {
187
+ const have = (_b = (_a = w.lookup.idFor(`heading ${m[1]}`)) !== null && _a !== void 0 ? _a : w.lookup.idFor(`Heading ${m[1]}`)) !== null && _b !== void 0 ? _b : (w.lookup.hasId(`Heading${m[1]}`) ? `Heading${m[1]}` : undefined);
188
+ if (have)
189
+ return have;
190
+ w.wanted.add(`Heading${m[1]}`);
191
+ return `Heading${m[1]}`;
192
+ }
193
+ const cls = (_c = el.getAttribute('class')) !== null && _c !== void 0 ? _c : '';
194
+ for (const c of cls.split(/\s+/)) {
195
+ if (c === '' || c === 'MsoNormal' || c === 'MsoListParagraph' || c === 'MsoListParagraphCxSpFirst' || c === 'MsoListParagraphCxSpMiddle' || c === 'MsoListParagraphCxSpLast')
196
+ continue;
197
+ const bare = c.startsWith('Mso') ? c.slice(3) : c;
198
+ if (w.lookup.hasId(bare))
199
+ return bare;
200
+ const spaced = bare.replace(/([a-z])([A-Z0-9])/g, '$1 $2');
201
+ const byName = (_d = w.lookup.idFor(spaced)) !== null && _d !== void 0 ? _d : w.lookup.idFor(bare);
202
+ if (byName)
203
+ return byName;
204
+ const builtIn = BUILT_IN[bare];
205
+ if (builtIn) {
206
+ w.wanted.add(builtIn);
207
+ return builtIn;
208
+ }
209
+ }
210
+ return null;
211
+ }
212
+ /** Word's `mso-list:l0 level2 lfo1` on a list paragraph. */
213
+ function listOf(el, w, ignoreLabel) {
214
+ var _a, _b, _c;
215
+ const mso = styleOf(el).get('mso-list');
216
+ const inList = w.listStack.length > 0;
217
+ if (!mso && !inList)
218
+ return undefined;
219
+ const level = mso ? Number((_b = (_a = /level(\d+)/.exec(mso)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '1') - 1 : Math.max(0, w.listStack.length - 1);
220
+ const lfo = mso ? (_c = /lfo(\d+)/.exec(mso)) === null || _c === void 0 ? void 0 : _c[1] : undefined;
221
+ const key = lfo ? `lfo${lfo}` : `list${w.listStack.length}`;
222
+ // Bullets: the enclosing list element says; without one, the label's glyph (a digit or a letter numbers, a symbol bullets).
223
+ let bullet;
224
+ if (inList)
225
+ bullet = w.listStack[w.listStack.length - 1];
226
+ else
227
+ bullet = ignoreLabel !== undefined ? BULLET_GLYPHS.test(ignoreLabel.trim()) || ignoreLabel.trim() === '' : true;
228
+ return { key, level: Math.max(0, Math.min(8, level)), bullet };
229
+ }
230
+ function withMark(state, el, w) {
231
+ var _a, _b, _c, _d, _e, _f, _g, _h;
232
+ const tag = el.tagName.toLowerCase();
233
+ const st = styleOf(el);
234
+ const next = { ...state };
235
+ if (tag === 'b' || tag === 'strong' || /^(bold|[6-9]00)$/.test((_a = st.get('font-weight')) !== null && _a !== void 0 ? _a : ''))
236
+ next.bold = true;
237
+ if (tag === 'i' || tag === 'em' || st.get('font-style') === 'italic')
238
+ next.italic = true;
239
+ if (tag === 'u' || ((_b = st.get('text-decoration')) !== null && _b !== void 0 ? _b : '').includes('underline'))
240
+ next.underline = true;
241
+ if (tag === 's' || tag === 'strike' || tag === 'del' || ((_c = st.get('text-decoration')) !== null && _c !== void 0 ? _c : '').includes('line-through'))
242
+ next.strike = true;
243
+ if (tag === 'sup' || st.get('vertical-align') === 'super')
244
+ next.vertAlign = 'superscript';
245
+ if (tag === 'sub' || st.get('vertical-align') === 'sub')
246
+ next.vertAlign = 'subscript';
247
+ // A link to the web or to mail is kept (ED-005 section 12.67), its relationship made when the paste
248
+ // lands; one to a place in its source (`#name`) or to a file is dropped and counted, its text kept.
249
+ // An `<a name>` with no `href` is a bookmark, not a link.
250
+ if (tag === 'a' && el.hasAttribute('href')) {
251
+ const address = webAddress((_d = el.getAttribute('href')) !== null && _d !== void 0 ? _d : '');
252
+ if (address !== undefined)
253
+ next.link = address;
254
+ else
255
+ w.drop('links');
256
+ }
257
+ // The direct formatting the element states (section 8.6): taken where it reads, counted as dropped where it does not.
258
+ if (st.has('font-family')) {
259
+ const name = fontNameOf(st.get('font-family'));
260
+ if (name)
261
+ next.fontName = name;
262
+ else
263
+ w.drop('fonts');
264
+ }
265
+ if (st.has('font-size')) {
266
+ const size = fontSizeOf(st.get('font-size'));
267
+ if (size)
268
+ next.fontSize = size;
269
+ else
270
+ w.drop('sizes');
271
+ }
272
+ if (st.has('color')) {
273
+ const color = colourOf(st.get('color'));
274
+ if (color)
275
+ next.color = color;
276
+ else
277
+ w.drop('colours');
278
+ }
279
+ if (st.has('mso-highlight') || st.has('background') || st.has('background-color')) {
280
+ const highlight = (_f = highlightHexOf((_e = st.get('mso-highlight')) !== null && _e !== void 0 ? _e : '')) !== null && _f !== void 0 ? _f : highlightHexOf((_h = (_g = st.get('background')) !== null && _g !== void 0 ? _g : st.get('background-color')) !== null && _h !== void 0 ? _h : '');
281
+ if (highlight)
282
+ next.highlight = highlight;
283
+ else
284
+ w.drop('highlights');
285
+ }
286
+ return next;
287
+ }
288
+ const RUN_FIELDS = ['bold', 'italic', 'underline', 'strike', 'vertAlign', 'fontName', 'fontSize', 'color', 'highlight', 'style', 'link'];
289
+ function pushText(inlines, text, state) {
290
+ const last = inlines[inlines.length - 1];
291
+ // After a break a line starts afresh: its leading space is not rendered.
292
+ if (last && 'br' in last)
293
+ text = text.replace(/^ +/, '');
294
+ if (text === '')
295
+ return;
296
+ const same = last && 'text' in last && RUN_FIELDS.every((f) => last[f] === state[f]);
297
+ if (same) {
298
+ last.text += text;
299
+ return;
300
+ }
301
+ inlines.push({ text, ...state });
302
+ }
303
+ /** The inline content of a paragraph-level element, whitespace collapsed as HTML renders it. */
304
+ function walkInline(node, inlines, state, w, pre) {
305
+ var _a;
306
+ if (node.nodeType === 3) {
307
+ const raw = (_a = node.textContent) !== null && _a !== void 0 ? _a : '';
308
+ const text = pre ? raw : raw.replace(/[ \t\r\n]+/g, ' ');
309
+ pushText(inlines, text, state);
310
+ return;
311
+ }
312
+ if (node.nodeType !== 1)
313
+ return;
314
+ const el = node;
315
+ const tag = el.tagName.toLowerCase();
316
+ if (tag === 'o:p' || tag === 'script' || tag === 'style')
317
+ return;
318
+ if (tag === 'br') {
319
+ inlines.push({ br: true });
320
+ return;
321
+ }
322
+ if (tag === 'img') {
323
+ w.drop('images');
324
+ return;
325
+ }
326
+ const st = styleOf(el);
327
+ if (st.has('mso-tab-count')) {
328
+ inlines.push({ tab: true });
329
+ return;
330
+ }
331
+ if (st.get('mso-list') === 'Ignore')
332
+ return;
333
+ const next = withMark(state, el, w);
334
+ el.childNodes.forEach((child) => walkInline(child, inlines, next, w, pre || tag === 'pre'));
335
+ }
336
+ /** The label span Word puts before a list paragraph's text (`mso-list:Ignore`), for the bullet-or-number decision. */
337
+ function ignoreLabelOf(el) {
338
+ var _a;
339
+ for (const span of Array.from(el.querySelectorAll('span'))) {
340
+ if (styleOf(span).get('mso-list') === 'Ignore')
341
+ return (_a = span.textContent) !== null && _a !== void 0 ? _a : '';
342
+ }
343
+ return undefined;
344
+ }
345
+ /** The paragraph formatting an element states (section 8.6): alignment, indents and the space before kept; `margin-bottom` and `line-height` dropped and counted. */
346
+ function paragraphFormatting(el, st, w) {
347
+ var _a, _b;
348
+ const out = {};
349
+ if (el.hasAttribute('align') || st.has('text-align')) {
350
+ const jc = jcOf((_b = (_a = st.get('text-align')) !== null && _a !== void 0 ? _a : el.getAttribute('align')) !== null && _b !== void 0 ? _b : undefined);
351
+ if (jc)
352
+ out.jc = jc;
353
+ else
354
+ w.drop('alignment');
355
+ }
356
+ if (st.has('margin-left') || st.has('text-indent') || st.has('margin-right')) {
357
+ const ind = {};
358
+ const start = twipsOf(st.get('margin-left'));
359
+ const end = twipsOf(st.get('margin-right'));
360
+ const first = twipsOf(st.get('text-indent'));
361
+ if (start !== undefined)
362
+ ind.start = start;
363
+ if (end !== undefined)
364
+ ind.end = end;
365
+ if (first !== undefined) {
366
+ if (first < 0)
367
+ ind.hanging = -first;
368
+ else
369
+ ind.firstLine = first;
370
+ }
371
+ if (Object.keys(ind).length > 0)
372
+ out.ind = ind;
373
+ else
374
+ w.drop('indents');
375
+ }
376
+ if (st.has('margin-top')) {
377
+ const before = twipsOf(st.get('margin-top'));
378
+ if (before !== undefined)
379
+ out.spacing = { before };
380
+ else
381
+ w.drop('spacing');
382
+ }
383
+ if (st.has('line-height') || st.has('margin-bottom'))
384
+ w.drop('spacing');
385
+ return out;
386
+ }
387
+ function paragraphFrom(el, w) {
388
+ const st = styleOf(el);
389
+ const formatting = paragraphFormatting(el, st, w);
390
+ const list = listOf(el, w, ignoreLabelOf(el));
391
+ if (list)
392
+ w.listKeys.add(list.key);
393
+ const inlines = [];
394
+ el.childNodes.forEach((child) => walkInline(child, inlines, {}, w, el.tagName.toLowerCase() === 'pre'));
395
+ trimEdges(inlines);
396
+ w.report.paragraphs++;
397
+ return { kind: 'paragraph', style: paragraphStyle(el, w), ...(list ? { list } : {}), ...formatting, inlines };
398
+ }
399
+ /** A paragraph's leading and trailing whitespace goes, as a browser renders it. */
400
+ function trimEdges(inlines) {
401
+ const first = inlines[0];
402
+ if (first && 'text' in first) {
403
+ first.text = first.text.replace(/^ +/, '');
404
+ if (first.text === '')
405
+ inlines.shift();
406
+ }
407
+ const last = inlines[inlines.length - 1];
408
+ if (last && 'text' in last) {
409
+ last.text = last.text.replace(/ +$/, '');
410
+ if (last.text === '')
411
+ inlines.pop();
412
+ }
413
+ }
414
+ function cellFrom(el, w) {
415
+ var _a, _b;
416
+ const blocks = [];
417
+ walkBlocks(el, blocks, w);
418
+ if (blocks.length === 0) {
419
+ blocks.push({ kind: 'paragraph', style: null, inlines: [] });
420
+ w.report.paragraphs++;
421
+ }
422
+ const cell = { blocks, colspan: Math.max(1, Number((_a = el.getAttribute('colspan')) !== null && _a !== void 0 ? _a : '1') || 1), rowspan: Math.max(1, Number((_b = el.getAttribute('rowspan')) !== null && _b !== void 0 ? _b : '1') || 1) };
423
+ const width = el.getAttribute('width');
424
+ const styleWidth = styleOf(el).get('width');
425
+ if (styleWidth && /pt$/.test(styleWidth))
426
+ cell.width = Math.round(parseFloat(styleWidth) * 20);
427
+ else if (width && /^\d+$/.test(width))
428
+ cell.width = Number(width) * 15;
429
+ return cell;
430
+ }
431
+ function tableFrom(el, w) {
432
+ const rows = [];
433
+ const rowElements = [];
434
+ const collect = (parent) => {
435
+ for (const child of Array.from(parent.children)) {
436
+ const tag = child.tagName.toLowerCase();
437
+ if (tag === 'tr')
438
+ rowElements.push(child);
439
+ else if (tag === 'thead' || tag === 'tbody' || tag === 'tfoot')
440
+ collect(child);
441
+ }
442
+ };
443
+ collect(el);
444
+ for (const tr of rowElements) {
445
+ const cells = [];
446
+ for (const child of Array.from(tr.children)) {
447
+ const tag = child.tagName.toLowerCase();
448
+ if (tag === 'td' || tag === 'th')
449
+ cells.push(cellFrom(child, w));
450
+ }
451
+ if (cells.length > 0)
452
+ rows.push(cells);
453
+ }
454
+ w.report.tables++;
455
+ return { kind: 'table', rows, style: w.tableStyle };
456
+ }
457
+ /** Block-level content in document order: paragraphs, headings, list items, tables; containers descended into. */
458
+ function walkBlocks(parent, out, w) {
459
+ var _a;
460
+ const pending = null;
461
+ void pending;
462
+ for (const node of Array.from(parent.childNodes)) {
463
+ if (node.nodeType === 3) {
464
+ // Bare text under a container (a cell without a paragraph): its own paragraph.
465
+ const text = ((_a = node.textContent) !== null && _a !== void 0 ? _a : '').replace(/[ \t\r\n]+/g, ' ').trim();
466
+ if (text !== '') {
467
+ out.push({ kind: 'paragraph', style: null, inlines: [{ text }] });
468
+ w.report.paragraphs++;
469
+ }
470
+ continue;
471
+ }
472
+ if (node.nodeType !== 1)
473
+ continue;
474
+ const el = node;
475
+ const tag = el.tagName.toLowerCase();
476
+ if (tag === 'script' || tag === 'style' || tag === 'head' || tag === 'meta' || tag === 'link' || tag === 'title' || tag === 'o:p')
477
+ continue;
478
+ if (tag === 'p' || HEADING.test(tag) || tag === 'li' || tag === 'pre' || tag === 'blockquote' && el.querySelector('p') === null) {
479
+ out.push(paragraphFrom(el, w));
480
+ continue;
481
+ }
482
+ if (tag === 'ul' || tag === 'ol') {
483
+ w.listStack.push(tag === 'ul');
484
+ walkBlocks(el, out, w);
485
+ w.listStack.pop();
486
+ continue;
487
+ }
488
+ if (tag === 'table') {
489
+ if (w.listStack.length === 0 || true)
490
+ out.push(tableFrom(el, w));
491
+ continue;
492
+ }
493
+ if (tag === 'img') {
494
+ w.drop('images');
495
+ continue;
496
+ }
497
+ if (tag === 'br')
498
+ continue;
499
+ // A container (div, body, html, span at block level, blockquote with paragraphs, td outside a table): its blocks.
500
+ walkBlocks(el, out, w);
501
+ }
502
+ }
503
+ /**
504
+ * Where a parsed fragment's elements are: a browser's `DOMParser` wraps a bare fragment (the text
505
+ * between Word's fragment markers, a data node's XHTML) in `html` and `body`; linkedom and other
506
+ * light DOMs leave the body empty and put them under the document element or the document. The
507
+ * first of those holding an element is the root.
508
+ */
509
+ function rootOf(doc) {
510
+ var _a, _b;
511
+ const html = doc.documentElement && /^html$/i.test(doc.documentElement.tagName) ? doc.documentElement : null;
512
+ for (const candidate of [doc.body, html, doc]) {
513
+ if (candidate && candidate.children.length > 0)
514
+ return candidate;
515
+ }
516
+ return (_b = (_a = doc.body) !== null && _a !== void 0 ? _a : doc.documentElement) !== null && _b !== void 0 ? _b : doc;
517
+ }
518
+ /** Convert clipboard HTML to the intermediate form, keeping what the outline names and counting the rest. */
519
+ export function convertHtml(html, lookup, options = {}) {
520
+ var _a;
521
+ const markup = clipboardMarkup(html);
522
+ const parse = (_a = options.parser) !== null && _a !== void 0 ? _a : ((h) => new DOMParser().parseFromString(h, 'text/html'));
523
+ const doc = parse(markup);
524
+ const report = { paragraphs: 0, lists: 0, tables: 0, dropped: {}, stylesWanted: [] };
525
+ const w = {
526
+ lookup,
527
+ report,
528
+ listKeys: new Set(),
529
+ wanted: new Set(),
530
+ listStack: [],
531
+ tableStyle: lookup.hasId('TableGrid') ? 'TableGrid' : null,
532
+ drop: (kind, times = 1) => { var _a; report.dropped[kind] = ((_a = report.dropped[kind]) !== null && _a !== void 0 ? _a : 0) + times; },
533
+ };
534
+ const blocks = [];
535
+ walkBlocks(rootOf(doc), blocks, w);
536
+ report.lists = w.listKeys.size;
537
+ report.stylesWanted = [...w.wanted];
538
+ return { blocks, report };
539
+ }
540
+ // ---------------------------------------------------------------- nodes of the schema
541
+ /** The list definitions a paste needs: a `w:numId` per list key. */