@portone/docx-editor 0.3.0 → 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 (90) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/DocxEditor.js +1 -0
  3. package/dist/core.d.ts +1 -1
  4. package/dist/docx/comments/writing.js +14 -8
  5. package/dist/docx/exportDocx.js +20 -4
  6. package/dist/docx/formatting/attrs.d.ts +20 -2
  7. package/dist/docx/formatting/attrs.js +57 -2
  8. package/dist/docx/formatting/context.d.ts +10 -2
  9. package/dist/docx/formatting/context.js +23 -5
  10. package/dist/docx/formatting/resolve.d.ts +10 -4
  11. package/dist/docx/formatting/resolve.js +31 -11
  12. package/dist/docx/formatting/styles.d.ts +25 -2
  13. package/dist/docx/formatting/styles.js +84 -3
  14. package/dist/docx/hyperlink.d.ts +14 -4
  15. package/dist/docx/hyperlink.js +4 -1
  16. package/dist/docx/importDocx.js +46 -37
  17. package/dist/docx/importTable.d.ts +2 -2
  18. package/dist/docx/importTable.js +52 -55
  19. package/dist/docx/invariants.js +26 -13
  20. package/dist/docx/newLists.d.ts +30 -0
  21. package/dist/docx/newLists.js +24 -1
  22. package/dist/docx/numberingPlanner.d.ts +6 -3
  23. package/dist/docx/numberingPlanner.js +38 -7
  24. package/dist/docx/session.d.ts +1 -1
  25. package/dist/docx/session.js +11 -1
  26. package/dist/docx/tableFormatting/conditions.d.ts +76 -0
  27. package/dist/docx/tableFormatting/conditions.js +196 -0
  28. package/dist/docx/tableFormatting/editing.d.ts +7 -3
  29. package/dist/docx/tableFormatting/editing.js +4 -5
  30. package/dist/docx/tableFormatting/reading.d.ts +49 -19
  31. package/dist/docx/tableFormatting/reading.js +109 -35
  32. package/dist/docx/tableFormatting.d.ts +1 -0
  33. package/dist/docx/tableFormatting.js +1 -0
  34. package/dist/docx/tableTemplate.js +12 -12
  35. package/dist/editor/commands/formatting/propertyCommands.js +6 -1
  36. package/dist/editor/commands/listCommands.d.ts +6 -3
  37. package/dist/editor/commands/listCommands.js +73 -28
  38. package/dist/editor/commands/paragraphCommands.js +6 -1
  39. package/dist/editor/createEditor.js +1 -1
  40. package/dist/editor/editorDocument.d.ts +7 -6
  41. package/dist/editor/editorDocument.js +18 -6
  42. package/dist/editor/externalClipboard.d.ts +13 -2
  43. package/dist/editor/externalClipboard.js +64 -34
  44. package/dist/editor/paragraphEdits.d.ts +16 -1
  45. package/dist/editor/paragraphEdits.js +13 -5
  46. package/dist/editor/paragraphPlacement.d.ts +11 -0
  47. package/dist/editor/paragraphPlacement.js +18 -0
  48. package/dist/editor/plugins/imagePaste.js +7 -4
  49. package/dist/editor/plugins/numberingDecorations.d.ts +10 -4
  50. package/dist/editor/plugins/numberingDecorations.js +43 -2
  51. package/dist/editor/plugins/paragraphDisplay.js +9 -3
  52. package/dist/editor/plugins/tableDisplay.js +19 -5
  53. package/dist/model/format.d.ts +50 -0
  54. package/dist/model/format.js +65 -0
  55. package/dist/numbering/listRegistry.d.ts +37 -0
  56. package/dist/numbering/listRegistry.js +112 -0
  57. package/dist/numbering/listTemplate.d.ts +26 -17
  58. package/dist/numbering/listTemplate.js +66 -20
  59. package/dist/numbering/markers.d.ts +15 -6
  60. package/dist/numbering/markers.js +26 -59
  61. package/dist/numbering/parseNumbering.d.ts +70 -5
  62. package/dist/numbering/parseNumbering.js +100 -26
  63. package/dist/numbering/spellers.d.ts +15 -0
  64. package/dist/numbering/spellers.js +96 -0
  65. package/dist/numbering/writeNumbering.d.ts +5 -3
  66. package/dist/numbering/writeNumbering.js +15 -15
  67. package/dist/ooxml/conformance.d.ts +28 -0
  68. package/dist/ooxml/conformance.js +20 -0
  69. package/dist/ooxml/errors.d.ts +5 -5
  70. package/dist/ooxml/names.d.ts +9 -0
  71. package/dist/ooxml/names.js +8 -1
  72. package/dist/ooxml/partSplice.d.ts +1 -1
  73. package/dist/ooxml/partSplice.js +10 -3
  74. package/dist/ooxml/xml.d.ts +8 -9
  75. package/dist/ooxml/xml.js +6 -6
  76. package/dist/page/PageGuides.d.ts +1 -1
  77. package/dist/page/PageGuides.js +0 -13
  78. package/dist/page/pageLayout.d.ts +0 -10
  79. package/dist/page/pageLayout.js +3 -10
  80. package/dist/page/usePageLayout.d.ts +0 -7
  81. package/dist/page/usePageLayout.js +2 -9
  82. package/dist/schema/attrRoles.js +8 -1
  83. package/dist/schema/docxSchema.js +32 -3
  84. package/dist/styles/classNames.d.ts +13 -2
  85. package/dist/styles/classNames.js +13 -2
  86. package/dist/styles.css +16 -18
  87. package/dist/table/cellFormatting.js +1 -2
  88. package/dist/table/gridBorders.d.ts +11 -15
  89. package/dist/table/gridBorders.js +32 -18
  90. package/package.json +1 -1
@@ -5,24 +5,25 @@ import {
5
5
  ST_TwipsMeasure
6
6
  } from "../ooxml/simpleTypes.js";
7
7
  import { readTabStopDirectives } from "../ooxml/tabStops.js";
8
- import { childValue, twipsToPt, wAttr } from "../ooxml/units.js";
8
+ import {
9
+ ALIGN_BY_JC,
10
+ childValue,
11
+ isOn,
12
+ twipsToPt,
13
+ wAttr
14
+ } from "../ooxml/units.js";
9
15
  import {
10
16
  childByLocalName,
11
17
  elementChildren,
12
18
  parseXml,
13
19
  withXmlParser
14
20
  } from "../ooxml/xml.js";
15
- var NUMBER_FORMATS = [
16
- "decimal",
17
- "bullet",
18
- "lowerLetter",
19
- "upperLetter",
20
- "lowerRoman"
21
- ];
22
- var EMPTY_NUMBERING = { lists: /* @__PURE__ */ new Map() };
23
- function isNumberFormat(value) {
24
- return NUMBER_FORMATS.some((format) => format === value);
25
- }
21
+ import { isNumberFormat } from "./spellers.js";
22
+ var NO_ADDED_LISTS = /* @__PURE__ */ new Map();
23
+ var EMPTY_NUMBERING = {
24
+ lists: /* @__PURE__ */ new Map(),
25
+ added: NO_ADDED_LISTS
26
+ };
26
27
  function indentOf(lvl) {
27
28
  const pPr = childByLocalName(lvl, "pPr");
28
29
  const ind = pPr ? childByLocalName(pPr, "ind") : null;
@@ -56,28 +57,53 @@ function levelIndentPt(indent) {
56
57
  textIndentPt: hangingPt !== null ? -hangingPt : twipsToPt(indent.firstLineTwips)
57
58
  };
58
59
  }
59
- function readLevel(lvl) {
60
+ var LEVEL_SUFFIXES = [
61
+ "tab",
62
+ "space",
63
+ "nothing"
64
+ ];
65
+ var LEVEL_ALIGNS = ["left", "center", "right"];
66
+ function suffixOf(lvl) {
67
+ const suffix = childValue(lvl, "suff");
68
+ return LEVEL_SUFFIXES.find((known) => known === suffix) ?? "tab";
69
+ }
70
+ function alignOf(lvl) {
71
+ const jc = childValue(lvl, "lvlJc");
72
+ const align = jc === null ? void 0 : ALIGN_BY_JC[jc];
73
+ return align === void 0 || align === "justify" ? "left" : align;
74
+ }
75
+ function restartAfterLevelOf(lvl) {
76
+ const restart = ST_DecimalNumber.parse(childValue(lvl, "lvlRestart"));
77
+ return restart === null ? null : restart - 1;
78
+ }
79
+ function readLevel(lvl, readRun) {
60
80
  const format = childValue(lvl, "numFmt");
61
81
  const pPr = childByLocalName(lvl, "pPr");
62
82
  const tabStops = readTabStopDirectives(pPr);
83
+ const rPr = childByLocalName(lvl, "rPr");
63
84
  return {
64
85
  format: isNumberFormat(format) ? format : "decimal",
65
86
  text: childValue(lvl, "lvlText") ?? "",
66
87
  start: ST_DecimalNumber.parse(childValue(lvl, "start")) ?? 1,
67
88
  indent: indentOf(lvl),
68
- ...tabStops.length === 0 ? {} : { tabStops }
89
+ ...tabStops.length === 0 ? {} : { tabStops },
90
+ restartAfterLevel: restartAfterLevelOf(lvl),
91
+ legal: isOn(lvl, "isLgl"),
92
+ suffix: suffixOf(lvl),
93
+ align: alignOf(lvl),
94
+ run: rPr && readRun ? readRun(rPr) : null
69
95
  };
70
96
  }
71
- function readLevels(parent) {
97
+ function readLevels(parent, readRun) {
72
98
  const levels = /* @__PURE__ */ new Map();
73
99
  for (const child of elementChildren(parent)) {
74
100
  if (child.localName !== "lvl") continue;
75
101
  const ilvl = ST_DecimalNumber.parse(wAttr(child, "ilvl"));
76
- if (ilvl !== null) levels.set(ilvl, readLevel(child));
102
+ if (ilvl !== null) levels.set(ilvl, readLevel(child, readRun));
77
103
  }
78
104
  return levels;
79
105
  }
80
- function readList(num, abstractLevels) {
106
+ function readList(num, abstractLevels, readRun) {
81
107
  const abstractNumId = ST_DecimalNumber.parse(
82
108
  childValue(num, "abstractNumId")
83
109
  );
@@ -88,7 +114,7 @@ function readList(num, abstractLevels) {
88
114
  const ilvl = ST_DecimalNumber.parse(wAttr(child, "ilvl"));
89
115
  if (ilvl === null) continue;
90
116
  const replacement = childByLocalName(child, "lvl");
91
- const base = replacement ? readLevel(replacement) : levels.get(ilvl);
117
+ const base = replacement ? readLevel(replacement, readRun) : levels.get(ilvl);
92
118
  if (!base) continue;
93
119
  const startOverride = ST_DecimalNumber.parse(
94
120
  childValue(child, "startOverride")
@@ -100,29 +126,77 @@ function readList(num, abstractLevels) {
100
126
  }
101
127
  return { levels };
102
128
  }
129
+ var NO_STYLE_LINKS = /* @__PURE__ */ new Map();
130
+ function levelsOf(id, definitions, resolved) {
131
+ const path = /* @__PURE__ */ new Set();
132
+ let current = id;
133
+ let levels = /* @__PURE__ */ new Map();
134
+ while (!path.has(current)) {
135
+ const known = resolved.get(current);
136
+ if (known) {
137
+ levels = known;
138
+ break;
139
+ }
140
+ path.add(current);
141
+ const el = definitions.byId.get(current);
142
+ if (!el) break;
143
+ levels = readLevels(el, definitions.readRun);
144
+ const styleId = childValue(el, "numStyleLink");
145
+ if (levels.size > 0 || styleId === null) break;
146
+ const named = definitions.links.get(styleId);
147
+ const deferred = (named === void 0 ? void 0 : definitions.ofList.get(named)) ?? definitions.ofStyle.get(styleId);
148
+ if (deferred === void 0) break;
149
+ current = deferred;
150
+ }
151
+ for (const seen of path) resolved.set(seen, levels);
152
+ return levels;
153
+ }
103
154
  function parseNumbering(xml, options) {
104
155
  if (xml === null) return EMPTY_NUMBERING;
105
- return withXmlParser(options?.xmlParser, () => readNumbering(xml));
156
+ return withXmlParser(options?.xmlParser, () => readNumbering(xml, options));
106
157
  }
107
- function readNumbering(xml) {
158
+ function readNumbering(xml, options) {
108
159
  const root = parseXml(xml).documentElement;
109
- const abstractLevels = /* @__PURE__ */ new Map();
160
+ const byId = /* @__PURE__ */ new Map();
161
+ const ofStyle = /* @__PURE__ */ new Map();
162
+ const ofList = /* @__PURE__ */ new Map();
110
163
  for (const child of elementChildren(root)) {
111
- if (child.localName !== "abstractNum") continue;
112
- const id = ST_DecimalNumber.parse(wAttr(child, "abstractNumId"));
113
- if (id !== null) abstractLevels.set(id, readLevels(child));
164
+ if (child.localName === "abstractNum") {
165
+ const id = ST_DecimalNumber.parse(wAttr(child, "abstractNumId"));
166
+ if (id === null) continue;
167
+ byId.set(id, child);
168
+ const styleId = childValue(child, "styleLink");
169
+ if (styleId !== null) ofStyle.set(styleId, id);
170
+ } else if (child.localName === "num") {
171
+ const numId = ST_DecimalNumber.parse(wAttr(child, "numId"));
172
+ const id = ST_DecimalNumber.parse(childValue(child, "abstractNumId"));
173
+ if (numId !== null && id !== null) ofList.set(numId, id);
174
+ }
175
+ }
176
+ const definitions = {
177
+ byId,
178
+ ofList,
179
+ ofStyle,
180
+ links: options?.links ?? NO_STYLE_LINKS,
181
+ readRun: options?.readRun
182
+ };
183
+ const abstractLevels = /* @__PURE__ */ new Map();
184
+ for (const id of byId.keys()) {
185
+ abstractLevels.set(id, levelsOf(id, definitions, abstractLevels));
114
186
  }
115
187
  const lists = /* @__PURE__ */ new Map();
116
188
  for (const child of elementChildren(root)) {
117
189
  if (child.localName !== "num") continue;
118
190
  const numId = ST_DecimalNumber.parse(wAttr(child, "numId"));
119
- const list = numId === null ? null : readList(child, abstractLevels);
191
+ const list = numId === null ? null : readList(child, abstractLevels, options?.readRun);
120
192
  if (numId !== null && list) lists.set(numId, list);
121
193
  }
122
- return { lists };
194
+ return { lists, added: NO_ADDED_LISTS };
123
195
  }
124
196
  export {
125
197
  EMPTY_NUMBERING,
198
+ LEVEL_ALIGNS,
199
+ LEVEL_SUFFIXES,
126
200
  NO_LEVEL_INDENT,
127
201
  levelIndentPt,
128
202
  parseNumbering
@@ -0,0 +1,15 @@
1
+ /**
2
+ * How each number format spells the count of a list item.
3
+ *
4
+ * `w:numFmt` names some sixty formats (§17.18.59). The ones here are the ones a marker is drawn
5
+ * in; a level naming any other counts in decimal instead. A format belongs to `NumberFormat`
6
+ * because it has a speller here, so a marker can never be counted in a format nothing can write.
7
+ */
8
+ /** A `w:numFmt` this editor spells out. Every other one is drawn as a decimal */
9
+ export type NumberFormat = "decimal" | "decimalZero" | "bullet" | "lowerLetter" | "upperLetter" | "lowerRoman" | "upperRoman" | "ganada" | "koreanDigital" | "chineseCounting";
10
+ export declare function isNumberFormat(value: string | null): value is NumberFormat;
11
+ /**
12
+ * The count as this format writes it, and as a decimal where spelling it out would run past the
13
+ * length a marker is ever drawn at.
14
+ */
15
+ export declare function spellNumber(count: number, format: NumberFormat, maxChars: number): string;
@@ -0,0 +1,96 @@
1
+ // src/numbering/spellers.ts
2
+ var ROMAN = [
3
+ [1e3, "M"],
4
+ [900, "CM"],
5
+ [500, "D"],
6
+ [400, "CD"],
7
+ [100, "C"],
8
+ [90, "XC"],
9
+ [50, "L"],
10
+ [40, "XL"],
11
+ [10, "X"],
12
+ [9, "IX"],
13
+ [5, "V"],
14
+ [4, "IV"],
15
+ [1, "I"]
16
+ ];
17
+ function toRoman(count) {
18
+ let rest = count;
19
+ let out = "";
20
+ for (const [amount, sign] of ROMAN) {
21
+ while (rest >= amount) {
22
+ out += sign;
23
+ rest -= amount;
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ function repeated(symbols, count) {
29
+ const index = (count - 1) % symbols.length;
30
+ const times = Math.floor((count - 1) / symbols.length) + 1;
31
+ return (symbols[index] ?? "").repeat(times);
32
+ }
33
+ var LATIN = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
34
+ var GANADA = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
35
+ var KOREAN_DIGITS = "\uC601\uC77C\uC774\uC0BC\uC0AC\uC624\uC721\uCE60\uD314\uAD6C";
36
+ function digits(symbols, count) {
37
+ return Array.from(`${count}`, (digit) => symbols[Number(digit)] ?? "").join(
38
+ ""
39
+ );
40
+ }
41
+ var CHINESE_DIGITS = "\u3007\u4E00\u4E8C\u4E09\u56DB\u4E94\u516D\u4E03\u516B\u4E5D\u5341";
42
+ function chineseCounting(count) {
43
+ if (count <= 10) return CHINESE_DIGITS[count] ?? "";
44
+ if (count >= 100) return digits(CHINESE_DIGITS, count);
45
+ const tens = Math.floor(count / 10);
46
+ const unit = count % 10;
47
+ return (tens === 1 ? "" : CHINESE_DIGITS[tens] ?? "") + CHINESE_DIGITS[10] + (unit === 0 ? "" : CHINESE_DIGITS[unit] ?? "");
48
+ }
49
+ var POSITIONAL = Number.POSITIVE_INFINITY;
50
+ var NUMBER_SPELLERS = {
51
+ decimal: { spell: (count) => `${count}`, countsPerChar: POSITIONAL },
52
+ /** The counts up to nine written with a zero in front (§17.18.59 decimalZero) */
53
+ decimalZero: {
54
+ spell: (count) => count < 10 ? `0${count}` : `${count}`,
55
+ countsPerChar: POSITIONAL
56
+ },
57
+ /**
58
+ * A bullet counts nothing: its `w:lvlText` is the symbol itself. A `%n` slot naming a bullet
59
+ * level has no symbol to put there, so the count goes in as a decimal, as Word draws it.
60
+ */
61
+ bullet: { spell: (count) => `${count}`, countsPerChar: POSITIONAL },
62
+ lowerLetter: {
63
+ spell: (count) => repeated(LATIN, count).toLowerCase(),
64
+ countsPerChar: LATIN.length
65
+ },
66
+ upperLetter: {
67
+ spell: (count) => repeated(LATIN, count),
68
+ countsPerChar: LATIN.length
69
+ },
70
+ lowerRoman: {
71
+ spell: (count) => toRoman(count).toLowerCase(),
72
+ countsPerChar: 1e3
73
+ },
74
+ upperRoman: { spell: toRoman, countsPerChar: 1e3 },
75
+ ganada: {
76
+ spell: (count) => repeated(GANADA, count),
77
+ countsPerChar: GANADA.length
78
+ },
79
+ koreanDigital: {
80
+ spell: (count) => digits(KOREAN_DIGITS, count),
81
+ countsPerChar: POSITIONAL
82
+ },
83
+ chineseCounting: { spell: chineseCounting, countsPerChar: POSITIONAL }
84
+ };
85
+ function isNumberFormat(value) {
86
+ return value !== null && Object.hasOwn(NUMBER_SPELLERS, value);
87
+ }
88
+ function spellNumber(count, format, maxChars) {
89
+ const speller = NUMBER_SPELLERS[format];
90
+ if (count < 1 || count > speller.countsPerChar * maxChars) return `${count}`;
91
+ return speller.spell(count);
92
+ }
93
+ export {
94
+ isNumberFormat,
95
+ spellNumber
96
+ };
@@ -1,12 +1,14 @@
1
1
  /**
2
- * Splices the definitions of newly started lists into the original numbering.xml.
2
+ * Splices the definitions the newly started lists were registered with into the original
3
+ * numbering.xml.
3
4
  *
4
5
  * Not one character of the original is altered; new elements are merely slotted in.
5
6
  * OOXML requires the definitions (`abstractNum`) to come before the numbers (`num`), and
6
7
  * `CHILD_ORDER.numbering` is what places each where it belongs among what the part holds.
7
8
  */
9
+ import type { NewList } from "./parseNumbering";
8
10
  /**
9
- * numbering.xml with a standard template definition added for every list number.
11
+ * numbering.xml with the definition each of these lists was started with added under its number.
10
12
  * When there is nothing to add, the original string is returned unchanged.
11
13
  */
12
- export declare function addListDefinitions(xml: string, numIds: readonly number[]): string;
14
+ export declare function addListDefinitions(xml: string, lists: ReadonlyMap<number, NewList>): string;
@@ -1,30 +1,30 @@
1
1
  // src/numbering/writeNumbering.ts
2
2
  import { splicePart } from "../ooxml/partSplice.js";
3
+ import { ST_DecimalNumber } from "../ooxml/simpleTypes.js";
3
4
  import { wAttr } from "../ooxml/units.js";
4
5
  import { elementChildren, parseXml } from "../ooxml/xml.js";
5
- import { abstractNumXml, listKindOf, numXml } from "./listTemplate.js";
6
- function maxAbstractNumId(xml) {
7
- let max = 0;
8
- for (const child of elementChildren(parseXml(xml).documentElement)) {
9
- if (child.localName !== "abstractNum") continue;
10
- const id = Number.parseInt(wAttr(child, "abstractNumId") ?? "", 10);
11
- if (Number.isFinite(id)) max = Math.max(max, id);
12
- }
13
- return max;
6
+ import { abstractNumXml, numberingIdAllocator, numXml } from "./listTemplate.js";
7
+ function abstractNumIds(xml) {
8
+ return elementChildren(parseXml(xml).documentElement).flatMap((child) => {
9
+ if (child.localName !== "abstractNum") return [];
10
+ const id = ST_DecimalNumber.parse(wAttr(child, "abstractNumId"));
11
+ return id === null ? [] : [id];
12
+ });
14
13
  }
15
- function addListDefinitions(xml, numIds) {
16
- if (numIds.length === 0) return xml;
17
- const firstAbstractNumId = maxAbstractNumId(xml) + 1;
18
- const additions = [...numIds].sort((a, b) => a - b).map((numId, index) => ({
14
+ function addListDefinitions(xml, lists) {
15
+ if (lists.size === 0) return xml;
16
+ const takeId = numberingIdAllocator(abstractNumIds(xml));
17
+ const additions = [...lists].sort(([left], [right]) => left - right).map(([numId, list]) => ({
19
18
  numId,
20
- abstractNumId: firstAbstractNumId + index
19
+ list,
20
+ abstractNumId: takeId()
21
21
  }));
22
22
  return splicePart(xml, {
23
23
  root: "numbering",
24
24
  insert: [
25
25
  ...additions.map((added) => ({
26
26
  name: "abstractNum",
27
- xml: abstractNumXml(added.abstractNumId, listKindOf(added.numId))
27
+ xml: abstractNumXml(added.abstractNumId, added.list)
28
28
  })),
29
29
  ...additions.map((added) => ({
30
30
  name: "num",
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Which conformance class a package is written in, and whether its main part is one this editor
3
+ * can write back into. Both are settled once, when the file is opened.
4
+ *
5
+ * ECMA-376 defines two document conformance classes, Strict (Part 1 §2.1) and Transitional
6
+ * (Part 4 §2.1). They describe the same vocabulary under different names: the main document part's
7
+ * root namespace and the relationship the package reaches it through differ between the two
8
+ * (Part 1 §11.3.10, Part 4 §9.2.10). This editor supports a subset of Transitional. Strict is
9
+ * refused explicitly so that its markup is never read using Transitional assumptions.
10
+ *
11
+ * A namespace prefix is the producer's own choice and carries no meaning of its own, so readers
12
+ * identify markup by namespace URI and local name. The WordprocessingML writer emits `w`
13
+ * (`ooxml/names`), so the main part's root must bind that prefix to the Transitional namespace.
14
+ */
15
+ /** The WordprocessingML namespace of a Strict main document part (Part 1 §11.3.10) */
16
+ export declare const STRICT_W_NS = "http://purl.oclc.org/ooxml/wordprocessingml/main";
17
+ /** The relationship a Strict package reaches its main document part through (Part 1 §11.3.10) */
18
+ export declare const STRICT_OFFICE_DOCUMENT_REL = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument";
19
+ export type Conformance = "transitional" | "strict";
20
+ /** The class this main part root is written in, null for a root of neither vocabulary */
21
+ export declare function conformanceOf(root: Element): Conformance | null;
22
+ /**
23
+ * Whether the root binds the prefix every writer emits (`w`) to the WordprocessingML namespace.
24
+ *
25
+ * A root has nothing above it to inherit a declaration from, so what it writes itself is the whole
26
+ * answer. A default namespace or another prefix alone does not bind `w`.
27
+ */
28
+ export declare function bindsWritingPrefix(root: Element): boolean;
@@ -0,0 +1,20 @@
1
+ // src/ooxml/conformance.ts
2
+ import { W_NS, W_PREFIX } from "./names.js";
3
+ var STRICT_W_NS = "http://purl.oclc.org/ooxml/wordprocessingml/main";
4
+ var STRICT_OFFICE_DOCUMENT_REL = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument";
5
+ function conformanceOf(root) {
6
+ if (root.namespaceURI === W_NS) return "transitional";
7
+ if (root.namespaceURI === STRICT_W_NS) return "strict";
8
+ return null;
9
+ }
10
+ function bindsWritingPrefix(root) {
11
+ return Array.from(root.attributes).some(
12
+ (attr) => attr.name === `xmlns:${W_PREFIX}` && attr.value === W_NS
13
+ );
14
+ }
15
+ export {
16
+ STRICT_OFFICE_DOCUMENT_REL,
17
+ STRICT_W_NS,
18
+ bindsWritingPrefix,
19
+ conformanceOf
20
+ };
@@ -14,9 +14,10 @@
14
14
  * - `missing-part`: the package has no main document part to read
15
15
  * - `missing-body`: the main part carries no `w:body`
16
16
  * - `malformed-xml`: the XML cannot be parsed, declares a DTD, or its markup is inconsistent
17
+ * - `unsupported-conformance`: the package is an ECMA-376 Strict one, and this editor reads Transitional
17
18
  * - `unsupported-content`: the document holds markup we cannot write back out unchanged
18
19
  */
19
- export type DocxImportErrorCode = "no-xml-parser" | "not-a-docx" | "too-large" | "missing-part" | "missing-body" | "malformed-xml" | "unsupported-content";
20
+ export type DocxImportErrorCode = "no-xml-parser" | "not-a-docx" | "too-large" | "missing-part" | "missing-body" | "malformed-xml" | "unsupported-conformance" | "unsupported-content";
20
21
  /** Thrown when we hit a document whose content cannot be kept safely. We refuse to open it instead of losing it silently */
21
22
  export declare class DocxImportError extends Error {
22
23
  readonly code: DocxImportErrorCode;
@@ -25,14 +26,13 @@ export declare class DocxImportError extends Error {
25
26
  /**
26
27
  * Why a document could not be written back out.
27
28
  *
28
- * - `missing-numbering-part`: a new list needs a numbering.xml the document does not have
29
- * - `missing-content-types`: a new image needs a [Content_Types].xml the package does not have
30
- * - `unsupported-content`: the document holds a node kind we have no way to serialize, or a preserved block standing in two places, which has one original XML to write
29
+ * - `missing-content-types`: a part the writer adds, be it an image, a list definition or a comment, needs a [Content_Types].xml the package does not have
30
+ * - `unsupported-content`: the document holds something no correct file can be written from: a node kind we have no way to serialize, a preserved block standing in two places, which has one original XML to write, or a paragraph in a list nothing defines
31
31
  * - `lost-original`: a node that only carries its original XML has lost it
32
32
  * - `malformed-xml`: an original XML fragment cannot be read well enough to rewrite
33
33
  * - `invalid-table`: the table grid is inconsistent, e.g. a vertical merge outliving its rows
34
34
  */
35
- export type DocxExportErrorCode = "missing-numbering-part" | "missing-content-types" | "unsupported-content" | "lost-original" | "malformed-xml" | "invalid-table";
35
+ export type DocxExportErrorCode = "missing-content-types" | "unsupported-content" | "lost-original" | "malformed-xml" | "invalid-table";
36
36
  /** Thrown when an edited document cannot be written back out without losing or corrupting content */
37
37
  export declare class DocxExportError extends Error {
38
38
  readonly code: DocxExportErrorCode;
@@ -6,6 +6,13 @@
6
6
  * here, and the part a fragment is spliced into declares the same binding, so the prefix is
7
7
  * decided once rather than at every call site.
8
8
  */
9
+ /** The wordprocessing namespace that every element we read lives in */
10
+ export declare const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
11
+ /**
12
+ * The relationship namespace. It is both where the `r:embed` attributes live and the base
13
+ * every relationship type name is built on
14
+ */
15
+ export declare const R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
9
16
  /** The prefix every WordprocessingML element and attribute this package writes carries */
10
17
  export declare const W_PREFIX = "w";
11
18
  export declare const NAMESPACES: {
@@ -19,6 +26,8 @@ export declare const NAMESPACES: {
19
26
  readonly pic: "http://schemas.openxmlformats.org/drawingml/2006/picture";
20
27
  };
21
28
  export type KnownPrefix = keyof typeof NAMESPACES;
29
+ /** Whether this is a prefix the package has a namespace for, rather than one a document brought */
30
+ export declare function isKnownPrefix(prefix: string): prefix is KnownPrefix;
22
31
  /** The name `local` goes out under (`qualify("w15", "commentEx")` -> `w15:commentEx`) */
23
32
  export declare function qualify(prefix: KnownPrefix, local: string): string;
24
33
  /** `wName("val")` -> `w:val`. The one place the written WordprocessingML prefix is decided */
@@ -1,5 +1,6 @@
1
1
  // src/ooxml/names.ts
2
- import { R_NS, W_NS } from "./xml.js";
2
+ var W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
3
+ var R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
3
4
  var W_PREFIX = "w";
4
5
  var NAMESPACES = {
5
6
  w: W_NS,
@@ -11,6 +12,9 @@ var NAMESPACES = {
11
12
  a: "http://schemas.openxmlformats.org/drawingml/2006/main",
12
13
  pic: "http://schemas.openxmlformats.org/drawingml/2006/picture"
13
14
  };
15
+ function isKnownPrefix(prefix) {
16
+ return Object.hasOwn(NAMESPACES, prefix);
17
+ }
14
18
  function qualify(prefix, local) {
15
19
  return `${prefix}:${local}`;
16
20
  }
@@ -22,7 +26,10 @@ function xmlnsDecl(prefix) {
22
26
  }
23
27
  export {
24
28
  NAMESPACES,
29
+ R_NS,
30
+ W_NS,
25
31
  W_PREFIX,
32
+ isKnownPrefix,
26
33
  qualify,
27
34
  wName,
28
35
  xmlnsDecl
@@ -62,6 +62,6 @@ export interface RootDeclarations {
62
62
  }
63
63
  /**
64
64
  * The part with its root binding every prefix named and its `mc:Ignorable` naming every token
65
- * asked for, each declared once: one the root declares already is left as it was written.
65
+ * asked for, each declared once: a matching declaration is preserved, and a conflicting binding is refused.
66
66
  */
67
67
  export declare function ensureRootDeclarations(xml: string, declarations: RootDeclarations): string;
@@ -121,12 +121,19 @@ function withDeclarations(openTag, { namespaces, ignorable }) {
121
121
  "the root element's attributes could not be read"
122
122
  );
123
123
  }
124
- const declared = new Set(attrs.map(([name]) => name));
124
+ const declared = new Map(attrs);
125
125
  const additions = [];
126
126
  for (const [prefix, namespace] of Object.entries(namespaces)) {
127
- if (namespace !== void 0 && !declared.has(`xmlns:${prefix}`)) {
128
- additions.push([`xmlns:${prefix}`, namespace]);
127
+ if (namespace === void 0) continue;
128
+ const name = `xmlns:${prefix}`;
129
+ const existing = declared.get(name);
130
+ if (existing !== void 0 && existing !== namespace) {
131
+ throw new DocxExportError(
132
+ "unsupported-content",
133
+ `the part root binds ${prefix} to a namespace the writer cannot use`
134
+ );
129
135
  }
136
+ if (existing === void 0) additions.push([name, namespace]);
130
137
  }
131
138
  const ignoring = IGNORABLE.exec(openTag);
132
139
  let tokens = null;
@@ -1,10 +1,5 @@
1
- /** The wordprocessing namespace that every element we read lives in */
2
- export declare const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
3
- /**
4
- * The relationship namespace. It is both where the `r:embed` attributes live and the base
5
- * every relationship type name is built on
6
- */
7
- export declare const R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
1
+ import { R_NS, W_NS } from "./names";
2
+ export { R_NS, W_NS };
8
3
  /**
9
4
  * The prefixes whose meaning the editor depends on, and the namespace each one has to carry.
10
5
  *
@@ -58,8 +53,12 @@ export declare function withXmlParser<T>(parser: XmlParser | undefined, work: ()
58
53
  export declare function parseXml(source: string): Document;
59
54
  /**
60
55
  * Gathers the namespace prefixes used in the fragment and declares them.
61
- * Only `w` carries real meaning; the rest are placeholders that keep the parser from stopping.
62
- * All we read are element names and `w:` attributes, so placeholders still let the values be read as they are.
56
+ *
57
+ * A prefix `NAMESPACES` knows is bound to the namespace it stands for, so an attribute read off
58
+ * the fragment by namespace answers what it would in the part the fragment came out of. A prefix
59
+ * it does not know is bound to a placeholder, which carries no meaning but keeps the parser going:
60
+ * a fragment naming `o:`, `v:` or `wne:` would otherwise fail to parse and be refused whole
61
+ * (`ooxml/fragment`), taking the node that held it with it.
63
62
  *
64
63
  * An attribute standing at the very start of the string counts too, since a fragment may be an
65
64
  * attribute list of its own (`attrString`) rather than an element.
package/dist/ooxml/xml.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // src/ooxml/xml.ts
2
2
  import { DocxImportError } from "./errors.js";
3
- var W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
4
- var R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
3
+ import { isKnownPrefix, NAMESPACES, R_NS, W_NS, W_PREFIX } from "./names.js";
5
4
  var RESERVED_PREFIXES = /* @__PURE__ */ new Map([
6
5
  ["w", W_NS],
7
6
  ["r", R_NS]
@@ -74,7 +73,7 @@ function parseXml(source) {
74
73
  return doc;
75
74
  }
76
75
  function namespaceDecls(xml) {
77
- const prefixes = /* @__PURE__ */ new Set(["w"]);
76
+ const prefixes = /* @__PURE__ */ new Set([W_PREFIX]);
78
77
  for (const [, prefix] of xml.matchAll(/<\/?([A-Za-z_][\w.-]*):/g)) {
79
78
  prefixes.add(prefix);
80
79
  }
@@ -85,9 +84,10 @@ function namespaceDecls(xml) {
85
84
  }
86
85
  prefixes.delete("xml");
87
86
  prefixes.delete("xmlns");
88
- return Array.from(prefixes).map(
89
- (prefix) => `xmlns:${prefix}="${prefix === "w" ? W_NS : `urn:docx-editor:${prefix}`}"`
90
- ).join(" ");
87
+ return Array.from(prefixes).map((prefix) => {
88
+ const namespace = isKnownPrefix(prefix) ? NAMESPACES[prefix] : `urn:docx-editor:${prefix}`;
89
+ return `xmlns:${prefix}="${namespace}"`;
90
+ }).join(" ");
91
91
  }
92
92
  function elementChildren(el) {
93
93
  return Array.from(el.children);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Draws the gaps between pages on the sheet, plus a number for each page.
2
+ * Draws the gaps between pages on the sheet.
3
3
  *
4
4
  * Straddling blocks have already been moved to the next page, so there is no text under
5
5
  * these bands.
@@ -6,9 +6,6 @@ import {
6
6
  } from "../docx/headersFooters.js";
7
7
  import { editorClassNames } from "../styles/classNames.js";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
- function badgeTitle(badge) {
10
- return badge.exactPage ? `Page ${badge.page}` : `Page ${badge.page} (approximate)`;
11
- }
12
9
  function PageGuides({
13
10
  overlay,
14
11
  headersFooters
@@ -37,16 +34,6 @@ function PageGuides({
37
34
  },
38
35
  mark.page
39
36
  )),
40
- overlay.badges.map((badge) => /* @__PURE__ */ jsx(
41
- "span",
42
- {
43
- className: editorClassNames.pageBadge,
44
- style: { top: `${badge.top}px` },
45
- title: badgeTitle(badge),
46
- children: badge.page
47
- },
48
- badge.page
49
- )),
50
37
  headersFooters && overlay.pages.flatMap((page) => {
51
38
  if (page.crossed) return [];
52
39
  const number = displayPageNumber(headersFooters, page.page);
@@ -63,11 +63,6 @@ export interface PageSplit {
63
63
  page: number;
64
64
  /** Whether the document records a break at this place */
65
65
  forced: boolean;
66
- /**
67
- * Whether every place before this one was recorded by the document, so the number can
68
- * be trusted
69
- */
70
- exactPage: boolean;
71
66
  /**
72
67
  * Whether the block is taller than one page, so it cannot be pushed and the text
73
68
  * crosses this place
@@ -79,11 +74,6 @@ export interface PageStart {
79
74
  page: number;
80
75
  /** Where this page's body starts, measured from the top of the body */
81
76
  bodyStart: number;
82
- /**
83
- * Whether every place before this one was recorded by the document, so the number can
84
- * be trusted
85
- */
86
- exactPage: boolean;
87
77
  /**
88
78
  * Whether text crosses over from the previous page, so this page continues with no top
89
79
  * margin