@docx4j/core-ts 0.3.2 → 0.5.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 (72) hide show
  1. package/README.md +8 -3
  2. package/dist/html/convert.d.mts +116 -0
  3. package/dist/html/convert.mjs +565 -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.d.mts +13 -1
  9. package/dist/model/content/Body.mjs +97 -19
  10. package/dist/model/content/Comment.mjs +7 -0
  11. package/dist/model/content/InlinePicture.d.mts +89 -10
  12. package/dist/model/content/InlinePicture.mjs +245 -35
  13. package/dist/model/content/List.mjs +10 -1
  14. package/dist/model/content/Paragraph.d.mts +28 -5
  15. package/dist/model/content/Paragraph.mjs +77 -37
  16. package/dist/model/content/Range.d.mts +18 -2
  17. package/dist/model/content/Range.mjs +194 -64
  18. package/dist/model/content/Table.d.mts +38 -0
  19. package/dist/model/content/Table.mjs +80 -2
  20. package/dist/model/content/TrackedChange.d.mts +129 -3
  21. package/dist/model/content/TrackedChange.mjs +577 -12
  22. package/dist/model/content/binder.d.mts +2 -1
  23. package/dist/model/content/binder.mjs +48 -1
  24. package/dist/model/content/index.d.mts +4 -3
  25. package/dist/model/content/index.mjs +3 -2
  26. package/dist/model/content/relationships.d.mts +30 -0
  27. package/dist/model/content/relationships.mjs +133 -0
  28. package/dist/model/content/stylesFacade.d.mts +4 -2
  29. package/dist/model/content/stylesFacade.mjs +20 -6
  30. package/dist/model/content/tableColumns.d.mts +48 -0
  31. package/dist/model/content/tableColumns.mjs +484 -0
  32. package/dist/model/content/tree.d.mts +52 -2
  33. package/dist/model/content/tree.mjs +48 -3
  34. package/dist/model/customxml/CustomXmlPartCollection.d.mts +6 -1
  35. package/dist/model/customxml/CustomXmlPartCollection.mjs +19 -3
  36. package/dist/model/customxml/bindings.d.mts +5 -2
  37. package/dist/model/customxml/bindings.mjs +21 -6
  38. package/dist/model/customxml/index.d.mts +3 -0
  39. package/dist/model/customxml/index.mjs +3 -0
  40. package/dist/model/customxml/opendope.d.mts +32 -0
  41. package/dist/model/customxml/opendope.mjs +56 -0
  42. package/dist/model/customxml/pictures.d.mts +57 -0
  43. package/dist/model/customxml/pictures.mjs +192 -0
  44. package/dist/model/customxml/xhtml.d.mts +42 -0
  45. package/dist/model/customxml/xhtml.mjs +162 -0
  46. package/dist/model/fonts/families.generated.mjs +2 -2
  47. package/dist/model/fonts/registry.mjs +7 -1
  48. package/dist/model/fonts/substitutions.generated.mjs +2 -2
  49. package/dist/model/fonts/themes.generated.mjs +1 -1
  50. package/dist/model/listnumbering/definitions.mjs +18 -0
  51. package/dist/model/properties/PropertyResolver.d.mts +27 -6
  52. package/dist/model/properties/PropertyResolver.mjs +83 -16
  53. package/dist/model/properties/catalogue.d.mts +15 -1
  54. package/dist/model/properties/catalogue.mjs +80 -27
  55. package/dist/model/properties/table.d.mts +16 -2
  56. package/dist/model/properties/table.mjs +13 -3
  57. package/dist/opc/Load.d.mts +9 -2
  58. package/dist/opc/index.d.mts +2 -2
  59. package/dist/opc/index.mjs +2 -2
  60. package/dist/opc/mce/preprocessor.d.mts +62 -6
  61. package/dist/opc/mce/preprocessor.mjs +142 -35
  62. package/dist/opc/mce/understood.d.mts +10 -0
  63. package/dist/opc/mce/understood.mjs +25 -3
  64. package/dist/packages/OpcPackage.d.mts +7 -0
  65. package/dist/packages/OpcPackage.mjs +15 -3
  66. package/dist/packages/WordprocessingMLPackage.d.mts +35 -2
  67. package/dist/packages/WordprocessingMLPackage.mjs +99 -2
  68. package/dist/parts/XmlPart.d.mts +26 -5
  69. package/dist/parts/XmlPart.mjs +67 -18
  70. package/dist/parts/wml/knownStyles.d.mts +1 -0
  71. package/dist/parts/wml/knownStyles.mjs +2233 -0
  72. package/package.json +7 -2
@@ -0,0 +1,565 @@
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
+ /** Elements a line holds: gathered with the text beside them into one paragraph when they stand under a container. */
458
+ const INLINE_TAGS = new Set(['span', 'a', 'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'ins', 'sub', 'sup', 'code', 'font', 'small', 'big', 'mark', 'abbr', 'cite', 'q', 'kbd', 'var', 'samp', 'tt', 'time', 'label', 'wbr', 'br']);
459
+ /** What makes an inline element a container after all: a block inside it (`<span><p>` in Word's HTML, `<a><div>`). */
460
+ const BLOCK_INSIDE = 'p,h1,h2,h3,h4,h5,h6,li,pre,blockquote,ul,ol,dl,table,div,section,article,header,footer,nav,aside,main,figure,figcaption,hr,img';
461
+ /**
462
+ * Block-level content in document order: paragraphs, headings, list items, tables; containers descended into.
463
+ * Text and inline elements met under a container (a cell without a paragraph; ChatGPT's note after a table, a
464
+ * `span` holding a `span` per word) are one paragraph together, up to the next block or the container's end,
465
+ * as a browser lays them out: a paragraph per text node had made a paragraph of every word.
466
+ */
467
+ function walkBlocks(parent, out, w) {
468
+ var _a;
469
+ let pending = null;
470
+ const flush = () => {
471
+ if (pending === null)
472
+ return;
473
+ while (pending.length > 0 && 'br' in pending[pending.length - 1])
474
+ pending.pop();
475
+ trimEdges(pending);
476
+ if (pending.length > 0) {
477
+ out.push({ kind: 'paragraph', style: null, inlines: pending });
478
+ w.report.paragraphs++;
479
+ }
480
+ pending = null;
481
+ };
482
+ for (const node of Array.from(parent.childNodes)) {
483
+ if (node.nodeType === 3) {
484
+ if (pending === null && ((_a = node.textContent) !== null && _a !== void 0 ? _a : '').trim() === '')
485
+ continue; // the markup's own line ends between blocks
486
+ pending !== null && pending !== void 0 ? pending : (pending = []);
487
+ walkInline(node, pending, {}, w, false);
488
+ continue;
489
+ }
490
+ if (node.nodeType !== 1)
491
+ continue;
492
+ const el = node;
493
+ const tag = el.tagName.toLowerCase();
494
+ if (tag === 'script' || tag === 'style' || tag === 'head' || tag === 'meta' || tag === 'link' || tag === 'title' || tag === 'o:p')
495
+ continue;
496
+ if (INLINE_TAGS.has(tag) && el.querySelector(BLOCK_INSIDE) === null) {
497
+ if (tag === 'br' && pending === null)
498
+ continue; // a break between blocks starts no paragraph
499
+ pending !== null && pending !== void 0 ? pending : (pending = []);
500
+ walkInline(el, pending, {}, w, false);
501
+ continue;
502
+ }
503
+ flush();
504
+ if (tag === 'p' || HEADING.test(tag) || tag === 'li' || tag === 'pre' || tag === 'blockquote' && el.querySelector('p') === null) {
505
+ out.push(paragraphFrom(el, w));
506
+ continue;
507
+ }
508
+ if (tag === 'ul' || tag === 'ol') {
509
+ w.listStack.push(tag === 'ul');
510
+ walkBlocks(el, out, w);
511
+ w.listStack.pop();
512
+ continue;
513
+ }
514
+ if (tag === 'table') {
515
+ out.push(tableFrom(el, w));
516
+ continue;
517
+ }
518
+ if (tag === 'img') {
519
+ w.drop('images');
520
+ continue;
521
+ }
522
+ // A container (div, body, html, a span holding blocks, blockquote with paragraphs, td outside a table): its blocks.
523
+ walkBlocks(el, out, w);
524
+ }
525
+ flush();
526
+ }
527
+ /**
528
+ * Where a parsed fragment's elements are: a browser's `DOMParser` wraps a bare fragment (the text
529
+ * between Word's fragment markers, a data node's XHTML) in `html` and `body`; linkedom and other
530
+ * light DOMs leave the body empty and put them under the document element or the document. The
531
+ * first of those holding an element is the root.
532
+ */
533
+ function rootOf(doc) {
534
+ var _a, _b;
535
+ const html = doc.documentElement && /^html$/i.test(doc.documentElement.tagName) ? doc.documentElement : null;
536
+ for (const candidate of [doc.body, html, doc]) {
537
+ if (candidate && candidate.children.length > 0)
538
+ return candidate;
539
+ }
540
+ return (_b = (_a = doc.body) !== null && _a !== void 0 ? _a : doc.documentElement) !== null && _b !== void 0 ? _b : doc;
541
+ }
542
+ /** Convert clipboard HTML to the intermediate form, keeping what the outline names and counting the rest. */
543
+ export function convertHtml(html, lookup, options = {}) {
544
+ var _a;
545
+ const markup = clipboardMarkup(html);
546
+ const parse = (_a = options.parser) !== null && _a !== void 0 ? _a : ((h) => new DOMParser().parseFromString(h, 'text/html'));
547
+ const doc = parse(markup);
548
+ const report = { paragraphs: 0, lists: 0, tables: 0, dropped: {}, stylesWanted: [] };
549
+ const w = {
550
+ lookup,
551
+ report,
552
+ listKeys: new Set(),
553
+ wanted: new Set(),
554
+ listStack: [],
555
+ tableStyle: lookup.hasId('TableGrid') ? 'TableGrid' : null,
556
+ drop: (kind, times = 1) => { var _a; report.dropped[kind] = ((_a = report.dropped[kind]) !== null && _a !== void 0 ? _a : 0) + times; },
557
+ };
558
+ const blocks = [];
559
+ walkBlocks(rootOf(doc), blocks, w);
560
+ report.lists = w.listKeys.size;
561
+ report.stylesWanted = [...w.wanted];
562
+ return { blocks, report };
563
+ }
564
+ // ---------------------------------------------------------------- nodes of the schema
565
+ /** The list definitions a paste needs: a `w:numId` per list key. */
@@ -0,0 +1,19 @@
1
+ import type { PasteBlock } from './convert.mjs';
2
+ /** Word's character style for a link's text, as `Range.hyperlink` gives it. */
3
+ export declare const HYPERLINK_STYLE = "Hyperlink";
4
+ /** What the blocks need from the document before they are built: a definition per list key, a relationship per link address. */
5
+ export interface BuildNeeds {
6
+ lists: {
7
+ key: string;
8
+ bullet: boolean;
9
+ }[];
10
+ links: string[];
11
+ }
12
+ /** The document's answers: `w:numId` per list key, `r:id` per address; undefined drops the list or the link (its text kept). */
13
+ export interface BuildResolvers {
14
+ numId?: (key: string) => string | undefined;
15
+ relId?: (address: string) => string | undefined;
16
+ }
17
+ export declare function needsOf(blocks: readonly PasteBlock[]): BuildNeeds;
18
+ /** The blocks as a WordprocessingML fragment: `w:p` and `w:tbl` elements, the prefixes undeclared (`insertXml` declares them). */
19
+ export declare function blocksToXml(blocks: readonly PasteBlock[], resolve?: BuildResolvers): string;