@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- package/package.json +94 -93
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
// OPC relationship resolution — the piece ADR-0020 deferred (zip-container.js:
|
|
2
|
-
// "real _rels/.rels relationship resolution is not implemented yet ... belongs
|
|
3
|
-
// in import.js"). A `<w:drawing>` picture references its bytes indirectly:
|
|
4
|
-
// `a:blip r:embed="rId5"` names a relationship id, and `word/_rels/
|
|
5
|
-
// document.xml.rels` maps that id to a part (`Target="media/image1.png"`).
|
|
6
|
-
// This resolves the id -> part-name link so the image render branch (Doc.images)
|
|
7
|
-
// can point at the retained `word/media/*` bytes.
|
|
8
|
-
//
|
|
9
|
-
// Deliberately narrow: it parses ONE relationships part into an id -> resolved
|
|
10
|
-
// part-name map. It reads the whole `<Relationship>` set (not just images) — the
|
|
11
|
-
// caller filters by what it looks up. Targets are resolved relative to the part
|
|
12
|
-
// the .rels belongs to (its base directory), the OPC rule; a "/"-prefixed Target
|
|
13
|
-
// is package-absolute. External targets (TargetMode="External", e.g. a hyperlink)
|
|
14
|
-
// are skipped — they are URLs, not package parts.
|
|
15
|
-
|
|
16
|
-
import { findChildren, getAttr } from "./xml-scan.js";
|
|
17
|
-
import { PKG_RELS_NS } from "./ns.js";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Collapse `..`/`.` segments in a package part path — Targets like
|
|
21
|
-
* `../media/image1.png` resolve against the base directory. Package paths use
|
|
22
|
-
* forward slashes and never a drive/host, so this stays a pure segment walk.
|
|
23
|
-
* @param {string} path
|
|
24
|
-
* @returns {string}
|
|
25
|
-
*/
|
|
26
|
-
function normalizePath(path) {
|
|
27
|
-
/** @type {string[]} */
|
|
28
|
-
const out = [];
|
|
29
|
-
for (const segment of path.split("/")) {
|
|
30
|
-
if (segment === "" || segment === ".") continue;
|
|
31
|
-
if (segment === "..") out.pop();
|
|
32
|
-
else out.push(segment);
|
|
33
|
-
}
|
|
34
|
-
return out.join("/");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* `Id -> resolved package part name` for one relationships part.
|
|
39
|
-
*
|
|
40
|
-
* @param {import("./xml-scan.js").XmlElement} root scanned `<Relationships>` root
|
|
41
|
-
* @param {string} baseDir the directory the owning part lives in, e.g. `"word"`
|
|
42
|
-
* for `word/document.xml` (whose rels is `word/_rels/document.xml.rels`).
|
|
43
|
-
* Relative Targets resolve against it.
|
|
44
|
-
* @returns {Map<string, string>}
|
|
45
|
-
*/
|
|
46
|
-
export function parseRelationships(root, baseDir) {
|
|
47
|
-
/** @type {Map<string, string>} */
|
|
48
|
-
const map = new Map();
|
|
49
|
-
const base = baseDir === "" ? "" : `${baseDir}/`;
|
|
50
|
-
for (const rel of findChildren(root, PKG_RELS_NS, "Relationship")) {
|
|
51
|
-
const id = getAttr(rel, "", "Id");
|
|
52
|
-
const target = getAttr(rel, "", "Target");
|
|
53
|
-
if (id === undefined || target === undefined) continue;
|
|
54
|
-
if (getAttr(rel, "", "TargetMode") === "External") continue; // a URL, not a part
|
|
55
|
-
const resolved = target.startsWith("/") ? normalizePath(target.slice(1)) : normalizePath(`${base}${target}`);
|
|
56
|
-
map.set(id, resolved);
|
|
57
|
-
}
|
|
58
|
-
return map;
|
|
59
|
-
}
|
|
1
|
+
// OPC relationship resolution — the piece ADR-0020 deferred (zip-container.js:
|
|
2
|
+
// "real _rels/.rels relationship resolution is not implemented yet ... belongs
|
|
3
|
+
// in import.js"). A `<w:drawing>` picture references its bytes indirectly:
|
|
4
|
+
// `a:blip r:embed="rId5"` names a relationship id, and `word/_rels/
|
|
5
|
+
// document.xml.rels` maps that id to a part (`Target="media/image1.png"`).
|
|
6
|
+
// This resolves the id -> part-name link so the image render branch (Doc.images)
|
|
7
|
+
// can point at the retained `word/media/*` bytes.
|
|
8
|
+
//
|
|
9
|
+
// Deliberately narrow: it parses ONE relationships part into an id -> resolved
|
|
10
|
+
// part-name map. It reads the whole `<Relationship>` set (not just images) — the
|
|
11
|
+
// caller filters by what it looks up. Targets are resolved relative to the part
|
|
12
|
+
// the .rels belongs to (its base directory), the OPC rule; a "/"-prefixed Target
|
|
13
|
+
// is package-absolute. External targets (TargetMode="External", e.g. a hyperlink)
|
|
14
|
+
// are skipped — they are URLs, not package parts.
|
|
15
|
+
|
|
16
|
+
import { findChildren, getAttr } from "./xml-scan.js";
|
|
17
|
+
import { PKG_RELS_NS } from "./ns.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Collapse `..`/`.` segments in a package part path — Targets like
|
|
21
|
+
* `../media/image1.png` resolve against the base directory. Package paths use
|
|
22
|
+
* forward slashes and never a drive/host, so this stays a pure segment walk.
|
|
23
|
+
* @param {string} path
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function normalizePath(path) {
|
|
27
|
+
/** @type {string[]} */
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const segment of path.split("/")) {
|
|
30
|
+
if (segment === "" || segment === ".") continue;
|
|
31
|
+
if (segment === "..") out.pop();
|
|
32
|
+
else out.push(segment);
|
|
33
|
+
}
|
|
34
|
+
return out.join("/");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `Id -> resolved package part name` for one relationships part.
|
|
39
|
+
*
|
|
40
|
+
* @param {import("./xml-scan.js").XmlElement} root scanned `<Relationships>` root
|
|
41
|
+
* @param {string} baseDir the directory the owning part lives in, e.g. `"word"`
|
|
42
|
+
* for `word/document.xml` (whose rels is `word/_rels/document.xml.rels`).
|
|
43
|
+
* Relative Targets resolve against it.
|
|
44
|
+
* @returns {Map<string, string>}
|
|
45
|
+
*/
|
|
46
|
+
export function parseRelationships(root, baseDir) {
|
|
47
|
+
/** @type {Map<string, string>} */
|
|
48
|
+
const map = new Map();
|
|
49
|
+
const base = baseDir === "" ? "" : `${baseDir}/`;
|
|
50
|
+
for (const rel of findChildren(root, PKG_RELS_NS, "Relationship")) {
|
|
51
|
+
const id = getAttr(rel, "", "Id");
|
|
52
|
+
const target = getAttr(rel, "", "Target");
|
|
53
|
+
if (id === undefined || target === undefined) continue;
|
|
54
|
+
if (getAttr(rel, "", "TargetMode") === "External") continue; // a URL, not a part
|
|
55
|
+
const resolved = target.startsWith("/") ? normalizePath(target.slice(1)) : normalizePath(`${base}${target}`);
|
|
56
|
+
map.set(id, resolved);
|
|
57
|
+
}
|
|
58
|
+
return map;
|
|
59
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Uint8Array} stylesXml word/styles.xml
|
|
3
|
+
* @param {Uint8Array | null} themeXml word/theme/theme1.xml, if the package has one
|
|
4
|
+
* @returns {(styleId: string | null, rPr: XmlElement | undefined) => ResolvedRunFont}
|
|
5
|
+
* `styleId`: the paragraph's pStyle, null for the default paragraph style
|
|
6
|
+
*/
|
|
7
|
+
export function runFontResolver(stylesXml: Uint8Array, themeXml: Uint8Array | null): (styleId: string | null, rPr: XmlElement | undefined) => ResolvedRunFont;
|
|
8
|
+
/**
|
|
9
|
+
* What checkFit needs of a run and of its paragraph (ADR-0042 Karar 4).
|
|
10
|
+
* @param {Uint8Array} stylesXml word/styles.xml
|
|
11
|
+
* @param {Uint8Array | null} themeXml
|
|
12
|
+
* @returns {{run: (styleId: string | null, rPr: XmlElement | undefined) => RunLayout, paragraph: (styleId: string | null, pPr: XmlElement | undefined) => ParagraphLayout}}
|
|
13
|
+
*/
|
|
14
|
+
export function layoutResolver(stylesXml: Uint8Array, themeXml: Uint8Array | null): {
|
|
15
|
+
run: (styleId: string | null, rPr: XmlElement | undefined) => RunLayout;
|
|
16
|
+
paragraph: (styleId: string | null, pPr: XmlElement | undefined) => ParagraphLayout;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The style a paragraph names (w:pPr/w:pStyle), or null.
|
|
20
|
+
* @param {XmlElement} paragraph
|
|
21
|
+
* @returns {string | null}
|
|
22
|
+
*/
|
|
23
|
+
export function paragraphStyleOf(paragraph: XmlElement): string | null;
|
|
24
|
+
export type XmlElement = import("./xml-scan.js").XmlElement;
|
|
25
|
+
export type RunFont = {
|
|
26
|
+
ascii: string | null;
|
|
27
|
+
hAnsi: string | null;
|
|
28
|
+
sizePt: number | null;
|
|
29
|
+
};
|
|
30
|
+
export type Unsupported = {
|
|
31
|
+
code: "CHARACTER_STYLE";
|
|
32
|
+
} | {
|
|
33
|
+
code: "THEME_FONT_WITHOUT_THEME";
|
|
34
|
+
name: string;
|
|
35
|
+
};
|
|
36
|
+
export type ResolvedRunFont = {
|
|
37
|
+
font: RunFont;
|
|
38
|
+
unsupported: Unsupported[];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* why a width cannot be known; `code` from a closed list (ADR-0042)
|
|
42
|
+
*/
|
|
43
|
+
export type Unknown = {
|
|
44
|
+
code: string;
|
|
45
|
+
property?: string;
|
|
46
|
+
};
|
|
47
|
+
export type RunLayout = {
|
|
48
|
+
font: RunFont;
|
|
49
|
+
/**
|
|
50
|
+
* w:sz; null when nothing sets it
|
|
51
|
+
*/
|
|
52
|
+
sizeHalfPoints: number | null;
|
|
53
|
+
bold: boolean;
|
|
54
|
+
italic: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* w:spacing, added after every character (ADR-0042 H5)
|
|
57
|
+
*/
|
|
58
|
+
spacingTwips: number;
|
|
59
|
+
/**
|
|
60
|
+
* w:rFonts@w:hint
|
|
61
|
+
*/
|
|
62
|
+
hint: string | null;
|
|
63
|
+
/**
|
|
64
|
+
* w:lang@w:eastAsia
|
|
65
|
+
*/
|
|
66
|
+
eastAsiaLanguage: string | null;
|
|
67
|
+
unknown: Unknown[];
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* measured from the box's inner left edge (ADR-0042 H6)
|
|
71
|
+
*/
|
|
72
|
+
export type TabStop = {
|
|
73
|
+
posTwips: number;
|
|
74
|
+
left: boolean;
|
|
75
|
+
};
|
|
76
|
+
export type ParagraphLayout = {
|
|
77
|
+
leftTwips: number;
|
|
78
|
+
rightTwips: number;
|
|
79
|
+
/**
|
|
80
|
+
* signed: a hanging indent is negative
|
|
81
|
+
*/
|
|
82
|
+
firstLineTwips: number;
|
|
83
|
+
jc: string;
|
|
84
|
+
/**
|
|
85
|
+
* sorted by position
|
|
86
|
+
*/
|
|
87
|
+
tabs: TabStop[];
|
|
88
|
+
unknown: Unknown[];
|
|
89
|
+
};
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// What a run draws with and how its paragraph is laid out, from the document
|
|
2
|
+
// defaults, then the paragraph's style chain (basedOn, root first, the derived
|
|
3
|
+
// style wins), then the element's own properties.
|
|
4
|
+
//
|
|
5
|
+
// Fonts: per slot (ascii, hAnsi), a theme attribute (asciiTheme/hAnsiTheme)
|
|
6
|
+
// beats the plain one at the same level and is looked up in the theme:
|
|
7
|
+
// minor* -> a:minorFont/a:latin, major* -> a:majorFont/a:latin. The slot logic
|
|
8
|
+
// is FR2-WORD's (docs/adr/evidence/fr2-word/bodytext_fonts.mjs, left as
|
|
9
|
+
// sealed). Moved here from templates/lib/fonts.mjs (FR3) for checkFit (FR4a,
|
|
10
|
+
// ADR-0042 Karar 4), which runs in the package.
|
|
11
|
+
//
|
|
12
|
+
// It refuses nothing: what it cannot resolve comes back in `unsupported` (the
|
|
13
|
+
// font) or `unknown` (the layout), and each caller decides — the manifest
|
|
14
|
+
// builder throws, checkFit says "unknown". The layout side is an ALLOWLIST: a
|
|
15
|
+
// property is either computed (fonts, size, bold, italic, letter spacing,
|
|
16
|
+
// indents, alignment, tab stops) or known to leave a line's width alone; any
|
|
17
|
+
// other one is a reason, never ignored.
|
|
18
|
+
//
|
|
19
|
+
// Not styles-resolve.js: that one throws on w:rStyle, does not read "off" and
|
|
20
|
+
// keeps firstLine and hanging apart; the fill engine never reaches it
|
|
21
|
+
// (test/docx-surface.test.js).
|
|
22
|
+
|
|
23
|
+
import { scanXml, findChild, findChildren, getAttr } from "./xml-scan.js";
|
|
24
|
+
import { W_NS, A_NS } from "./ns.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {import("./xml-scan.js").XmlElement} XmlElement
|
|
28
|
+
* @typedef {{ascii: string | null, hAnsi: string | null, sizePt: number | null}} RunFont
|
|
29
|
+
* @typedef {{code: "CHARACTER_STYLE"} | {code: "THEME_FONT_WITHOUT_THEME", name: string}} Unsupported
|
|
30
|
+
* @typedef {{font: RunFont, unsupported: Unsupported[]}} ResolvedRunFont
|
|
31
|
+
* @typedef {{code: string, property?: string}} Unknown why a width cannot be known; `code` from a closed list (ADR-0042)
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} RunLayout
|
|
36
|
+
* @property {RunFont} font
|
|
37
|
+
* @property {number | null} sizeHalfPoints w:sz; null when nothing sets it
|
|
38
|
+
* @property {boolean} bold
|
|
39
|
+
* @property {boolean} italic
|
|
40
|
+
* @property {number} spacingTwips w:spacing, added after every character (ADR-0042 H5)
|
|
41
|
+
* @property {string | null} hint w:rFonts@w:hint
|
|
42
|
+
* @property {string | null} eastAsiaLanguage w:lang@w:eastAsia
|
|
43
|
+
* @property {Unknown[]} unknown
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {{posTwips: number, left: boolean}} TabStop measured from the box's inner left edge (ADR-0042 H6)
|
|
48
|
+
* @typedef {object} ParagraphLayout
|
|
49
|
+
* @property {number} leftTwips
|
|
50
|
+
* @property {number} rightTwips
|
|
51
|
+
* @property {number} firstLineTwips signed: a hanging indent is negative
|
|
52
|
+
* @property {string} jc
|
|
53
|
+
* @property {TabStop[]} tabs sorted by position
|
|
54
|
+
* @property {Unknown[]} unknown
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
// Run properties checkFit computes, or knows to leave a line's width alone: colour, underline,
|
|
58
|
+
// language, highlight, shading, proofing, the complex-script twins and a vertical shift.
|
|
59
|
+
const RUN_PROPERTIES = new Set(["rFonts", "sz", "szCs", "b", "bCs", "i", "iCs", "spacing", "color", "u", "lang", "highlight", "shd", "noProof", "position", "vertAlign"]);
|
|
60
|
+
// Paragraph properties likewise: vertical spacing, pagination, East Asian line rules a Latin line
|
|
61
|
+
// does not reach, the paragraph mark's own run properties (it never breaks a line, ADR-0042 H4).
|
|
62
|
+
// numPr, textDirection, bidi and mirrorIndents pass only with the value that turns them off.
|
|
63
|
+
const PARAGRAPH_PROPERTIES = new Set([
|
|
64
|
+
"pStyle", "keepNext", "keepLines", "pageBreakBefore", "widowControl", "suppressLineNumbers", "suppressAutoHyphens",
|
|
65
|
+
"spacing", "ind", "contextualSpacing", "jc", "tabs", "rPr", "outlineLvl", "snapToGrid", "autoSpaceDE", "autoSpaceDN",
|
|
66
|
+
"adjustRightInd", "wordWrap", "kinsoku", "overflowPunct", "topLinePunct", "textAlignment", "shd", "numPr",
|
|
67
|
+
"textDirection", "bidi", "mirrorIndents",
|
|
68
|
+
]);
|
|
69
|
+
const JUSTIFICATION = new Set(["left", "start", "center", "right", "end", "both"]);
|
|
70
|
+
const ON = new Set(["true", "1", "on"]);
|
|
71
|
+
const OFF = new Set(["false", "0", "off"]);
|
|
72
|
+
const INTEGER = /^-?[0-9]+$/;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ST_OnOff (ECMA-376 §22.9.2.7): no w:val is on.
|
|
76
|
+
* @param {XmlElement} el
|
|
77
|
+
* @returns {boolean | undefined} undefined when unreadable
|
|
78
|
+
*/
|
|
79
|
+
function onOff(el) {
|
|
80
|
+
const val = getAttr(el, W_NS, "val");
|
|
81
|
+
if (val === undefined || ON.has(val)) return true;
|
|
82
|
+
return OFF.has(val) ? false : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {XmlElement} el
|
|
87
|
+
* @returns {XmlElement[]}
|
|
88
|
+
*/
|
|
89
|
+
const elementChildren = (el) => /** @type {XmlElement[]} */ (el.children.filter((c) => c.kind === "element"));
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The parts of word/styles.xml (and the theme) both resolvers read.
|
|
93
|
+
* @param {Uint8Array} stylesXml
|
|
94
|
+
* @param {Uint8Array | null} themeXml
|
|
95
|
+
*/
|
|
96
|
+
function readStyles(stylesXml, themeXml) {
|
|
97
|
+
const root = scanXml(stylesXml).root;
|
|
98
|
+
const theme = themeXml ? scanXml(themeXml).root : null;
|
|
99
|
+
/** @param {string} name @returns {string | null} */
|
|
100
|
+
const themeFont = (name) => {
|
|
101
|
+
if (theme === null) return null;
|
|
102
|
+
const elements = findChild(theme, A_NS, "themeElements");
|
|
103
|
+
const scheme = elements && findChild(elements, A_NS, "fontScheme");
|
|
104
|
+
const set = scheme && findChild(scheme, A_NS, name.startsWith("major") ? "majorFont" : "minorFont");
|
|
105
|
+
const latin = set && findChild(set, A_NS, "latin");
|
|
106
|
+
return latin ? (getAttr(latin, "", "typeface") ?? null) : null;
|
|
107
|
+
};
|
|
108
|
+
const paragraphStyles = findChildren(root, W_NS, "style").filter((s) => getAttr(s, W_NS, "type") === "paragraph");
|
|
109
|
+
const byId = new Map(paragraphStyles.map((s) => [getAttr(s, W_NS, "styleId"), s]));
|
|
110
|
+
const defaultStyle = paragraphStyles.find((s) => getAttr(s, W_NS, "default") === "1");
|
|
111
|
+
const defaultId = defaultStyle ? (getAttr(defaultStyle, W_NS, "styleId") ?? null) : null;
|
|
112
|
+
const docDefaults = findChild(root, W_NS, "docDefaults");
|
|
113
|
+
const rPrDefault = docDefaults && findChild(docDefaults, W_NS, "rPrDefault");
|
|
114
|
+
const pPrDefault = docDefaults && findChild(docDefaults, W_NS, "pPrDefault");
|
|
115
|
+
/**
|
|
116
|
+
* The paragraph style chain, root first.
|
|
117
|
+
* @param {string | null} styleId null for the default paragraph style
|
|
118
|
+
* @returns {XmlElement[]}
|
|
119
|
+
*/
|
|
120
|
+
const chainOf = (styleId) => {
|
|
121
|
+
/** @type {XmlElement[]} */
|
|
122
|
+
const chain = [];
|
|
123
|
+
for (let id = styleId ?? defaultId; id && byId.has(id); ) {
|
|
124
|
+
const style = /** @type {XmlElement} */ (byId.get(id));
|
|
125
|
+
if (chain.includes(style)) break;
|
|
126
|
+
chain.push(style);
|
|
127
|
+
const basedOn = findChild(style, W_NS, "basedOn");
|
|
128
|
+
id = basedOn ? (getAttr(basedOn, W_NS, "val") ?? null) : null;
|
|
129
|
+
}
|
|
130
|
+
return chain.reverse();
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
theme,
|
|
134
|
+
themeFont,
|
|
135
|
+
chainOf,
|
|
136
|
+
/** @param {string} id */
|
|
137
|
+
hasStyle: (id) => byId.has(id),
|
|
138
|
+
defaultsRPr: rPrDefault && findChild(rPrDefault, W_NS, "rPr"),
|
|
139
|
+
defaultsPPr: pPrDefault && findChild(pPrDefault, W_NS, "pPr"),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {Uint8Array} stylesXml word/styles.xml
|
|
145
|
+
* @param {Uint8Array | null} themeXml word/theme/theme1.xml, if the package has one
|
|
146
|
+
* @returns {(styleId: string | null, rPr: XmlElement | undefined) => ResolvedRunFont}
|
|
147
|
+
* `styleId`: the paragraph's pStyle, null for the default paragraph style
|
|
148
|
+
*/
|
|
149
|
+
export function runFontResolver(stylesXml, themeXml) {
|
|
150
|
+
const { theme, themeFont, chainOf, defaultsRPr } = readStyles(stylesXml, themeXml);
|
|
151
|
+
return (styleId, rPr) => {
|
|
152
|
+
/** @type {RunFont} */
|
|
153
|
+
const font = { ascii: null, hAnsi: null, sizePt: null };
|
|
154
|
+
/** @type {Unsupported[]} */
|
|
155
|
+
const unsupported = [];
|
|
156
|
+
/** @param {XmlElement | undefined} el */
|
|
157
|
+
const apply = (el) => {
|
|
158
|
+
if (!el) return;
|
|
159
|
+
if (findChild(el, W_NS, "rStyle") && !unsupported.some((u) => u.code === "CHARACTER_STYLE")) unsupported.push({ code: "CHARACTER_STYLE" });
|
|
160
|
+
const rFonts = findChild(el, W_NS, "rFonts");
|
|
161
|
+
if (rFonts) {
|
|
162
|
+
for (const slot of /** @type {const} */ (["ascii", "hAnsi"])) {
|
|
163
|
+
const themed = getAttr(rFonts, W_NS, `${slot}Theme`);
|
|
164
|
+
const plain = getAttr(rFonts, W_NS, slot);
|
|
165
|
+
if (themed !== undefined) {
|
|
166
|
+
if (theme === null) unsupported.push({ code: "THEME_FONT_WITHOUT_THEME", name: themed });
|
|
167
|
+
font[slot] = themeFont(themed);
|
|
168
|
+
} else if (plain !== undefined) font[slot] = plain;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const sz = findChild(el, W_NS, "sz");
|
|
172
|
+
const val = sz && getAttr(sz, W_NS, "val");
|
|
173
|
+
if (val !== undefined && /^[0-9]+$/.test(val)) font.sizePt = Number(val) / 2;
|
|
174
|
+
};
|
|
175
|
+
apply(defaultsRPr);
|
|
176
|
+
for (const style of chainOf(styleId)) apply(findChild(style, W_NS, "rPr"));
|
|
177
|
+
apply(rPr);
|
|
178
|
+
return { font, unsupported };
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* What checkFit needs of a run and of its paragraph (ADR-0042 Karar 4).
|
|
184
|
+
* @param {Uint8Array} stylesXml word/styles.xml
|
|
185
|
+
* @param {Uint8Array | null} themeXml
|
|
186
|
+
* @returns {{run: (styleId: string | null, rPr: XmlElement | undefined) => RunLayout, paragraph: (styleId: string | null, pPr: XmlElement | undefined) => ParagraphLayout}}
|
|
187
|
+
*/
|
|
188
|
+
export function layoutResolver(stylesXml, themeXml) {
|
|
189
|
+
const { chainOf, hasStyle, defaultsRPr, defaultsPPr } = readStyles(stylesXml, themeXml);
|
|
190
|
+
const fontOf = runFontResolver(stylesXml, themeXml);
|
|
191
|
+
// A pStyle naming no paragraph style: which style Word falls back to is not known here.
|
|
192
|
+
/** @param {string | null} styleId @returns {Unknown[]} */
|
|
193
|
+
const missing = (styleId) => (styleId !== null && !hasStyle(styleId) ? [{ code: "STYLE_MISSING", property: "pStyle" }] : []);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {string | null} styleId
|
|
197
|
+
* @param {XmlElement | undefined} rPr
|
|
198
|
+
* @returns {RunLayout}
|
|
199
|
+
*/
|
|
200
|
+
const run = (styleId, rPr) => {
|
|
201
|
+
const { font, unsupported } = fontOf(styleId, rPr);
|
|
202
|
+
/** @type {Unknown[]} */
|
|
203
|
+
const unknown = [];
|
|
204
|
+
/** @param {Unknown} u */
|
|
205
|
+
const add = (u) => {
|
|
206
|
+
if (!unknown.some((x) => x.code === u.code && x.property === u.property)) unknown.push(u);
|
|
207
|
+
};
|
|
208
|
+
for (const u of unsupported) add({ code: u.code });
|
|
209
|
+
/** @type {string | undefined} */ let sz;
|
|
210
|
+
/** @type {string | undefined} */ let spacing;
|
|
211
|
+
/** @type {string | undefined} */ let hint;
|
|
212
|
+
/** @type {string | undefined} */ let eastAsia;
|
|
213
|
+
/** @type {Record<"b" | "i", {styles: (boolean | undefined)[], direct: boolean | undefined | null}>} */
|
|
214
|
+
const toggles = { b: { styles: [], direct: null }, i: { styles: [], direct: null } };
|
|
215
|
+
/**
|
|
216
|
+
* @param {XmlElement | undefined} el
|
|
217
|
+
* @param {boolean} direct
|
|
218
|
+
*/
|
|
219
|
+
const apply = (el, direct) => {
|
|
220
|
+
if (!el) return;
|
|
221
|
+
for (const c of elementChildren(el)) {
|
|
222
|
+
if (c.uri !== W_NS || !RUN_PROPERTIES.has(c.localName)) {
|
|
223
|
+
if (!(c.uri === W_NS && c.localName === "rStyle")) add({ code: "RUN_PROPERTY", property: c.localName });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const val = getAttr(c, W_NS, "val");
|
|
227
|
+
if (c.localName === "sz" && val !== undefined) sz = val;
|
|
228
|
+
else if (c.localName === "spacing" && val !== undefined) spacing = val;
|
|
229
|
+
else if (c.localName === "rFonts") hint = getAttr(c, W_NS, "hint") ?? hint;
|
|
230
|
+
else if (c.localName === "lang") eastAsia = getAttr(c, W_NS, "eastAsia") ?? eastAsia;
|
|
231
|
+
else if (c.localName === "vertAlign" && val !== "baseline") add({ code: "RUN_PROPERTY", property: "vertAlign" });
|
|
232
|
+
else if (c.localName === "b" || c.localName === "i") {
|
|
233
|
+
const t = toggles[c.localName];
|
|
234
|
+
if (direct) t.direct = onOff(c);
|
|
235
|
+
else t.styles.push(onOff(c));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
for (const u of missing(styleId)) add(u);
|
|
240
|
+
apply(defaultsRPr, false);
|
|
241
|
+
for (const style of chainOf(styleId)) apply(findChild(style, W_NS, "rPr"), false);
|
|
242
|
+
apply(rPr, true);
|
|
243
|
+
|
|
244
|
+
/** @param {"b" | "i"} name @returns {boolean} */
|
|
245
|
+
const toggle = (name) => {
|
|
246
|
+
const t = toggles[name];
|
|
247
|
+
// Direct formatting sets a toggle property's state; in the style hierarchy ECMA-376 §17.7.3
|
|
248
|
+
// toggles it instead. One style-side setting reads the same either way; two do not.
|
|
249
|
+
const value = t.direct !== null ? t.direct : t.styles.length === 0 ? false : t.styles.length === 1 ? t.styles[0] : undefined;
|
|
250
|
+
if (value === undefined) {
|
|
251
|
+
add({ code: t.direct === null && t.styles.length > 1 ? "TOGGLE_IN_STYLES" : "RUN_PROPERTY_UNREADABLE", property: name });
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
};
|
|
256
|
+
const bold = toggle("b");
|
|
257
|
+
const italic = toggle("i");
|
|
258
|
+
if (sz !== undefined && !/^[0-9]+$/.test(sz)) add({ code: "RUN_PROPERTY_UNREADABLE", property: "sz" });
|
|
259
|
+
if (spacing !== undefined && !INTEGER.test(spacing)) add({ code: "RUN_PROPERTY_UNREADABLE", property: "spacing" });
|
|
260
|
+
return {
|
|
261
|
+
font,
|
|
262
|
+
sizeHalfPoints: sz !== undefined && /^[0-9]+$/.test(sz) ? Number(sz) : null,
|
|
263
|
+
bold,
|
|
264
|
+
italic,
|
|
265
|
+
spacingTwips: spacing !== undefined && INTEGER.test(spacing) ? Number(spacing) : 0,
|
|
266
|
+
hint: hint ?? null,
|
|
267
|
+
eastAsiaLanguage: eastAsia ?? null,
|
|
268
|
+
unknown,
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {string | null} styleId
|
|
274
|
+
* @param {XmlElement | undefined} pPr
|
|
275
|
+
* @returns {ParagraphLayout}
|
|
276
|
+
*/
|
|
277
|
+
const paragraph = (styleId, pPr) => {
|
|
278
|
+
/** @type {Unknown[]} */
|
|
279
|
+
const unknown = [];
|
|
280
|
+
/** @param {Unknown} u */
|
|
281
|
+
const add = (u) => {
|
|
282
|
+
if (!unknown.some((x) => x.code === u.code && x.property === u.property)) unknown.push(u);
|
|
283
|
+
};
|
|
284
|
+
/** @type {Record<"left" | "right" | "firstLine", string | undefined>} */
|
|
285
|
+
const ind = { left: undefined, right: undefined, firstLine: undefined };
|
|
286
|
+
let jc = "left";
|
|
287
|
+
/** @type {Map<number, boolean>} */
|
|
288
|
+
const tabs = new Map();
|
|
289
|
+
/** @type {string | undefined} */ let numId;
|
|
290
|
+
/** @param {XmlElement | undefined} el */
|
|
291
|
+
const apply = (el) => {
|
|
292
|
+
if (!el) return;
|
|
293
|
+
for (const c of elementChildren(el)) {
|
|
294
|
+
if (c.uri !== W_NS || !PARAGRAPH_PROPERTIES.has(c.localName)) {
|
|
295
|
+
add({ code: "PARAGRAPH_PROPERTY", property: c.localName });
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const val = getAttr(c, W_NS, "val");
|
|
299
|
+
if (c.localName === "ind") {
|
|
300
|
+
for (const a of c.attributes) {
|
|
301
|
+
if (a.uri === W_NS && a.localName.endsWith("Chars")) add({ code: "INDENT_CHARS", property: a.localName });
|
|
302
|
+
}
|
|
303
|
+
/** @param {"left" | "right"} side @param {string} synonym */
|
|
304
|
+
const edge = (side, synonym) => {
|
|
305
|
+
const plain = getAttr(c, W_NS, side);
|
|
306
|
+
const other = getAttr(c, W_NS, synonym);
|
|
307
|
+
if (plain !== undefined && other !== undefined && plain !== other) add({ code: "INDENT_CONFLICT", property: side });
|
|
308
|
+
const v = other ?? plain;
|
|
309
|
+
if (v !== undefined) ind[side] = v;
|
|
310
|
+
};
|
|
311
|
+
edge("left", "start");
|
|
312
|
+
edge("right", "end");
|
|
313
|
+
// ECMA-376 §17.3.1.12: with both on one level, hanging wins.
|
|
314
|
+
const hanging = getAttr(c, W_NS, "hanging");
|
|
315
|
+
const firstLine = getAttr(c, W_NS, "firstLine");
|
|
316
|
+
if (hanging !== undefined) ind.firstLine = INTEGER.test(hanging) ? String(-Number(hanging)) : hanging;
|
|
317
|
+
else if (firstLine !== undefined) ind.firstLine = firstLine;
|
|
318
|
+
} else if (c.localName === "jc" && val !== undefined) {
|
|
319
|
+
jc = val;
|
|
320
|
+
} else if (c.localName === "tabs") {
|
|
321
|
+
for (const tab of findChildren(c, W_NS, "tab")) {
|
|
322
|
+
const pos = getAttr(tab, W_NS, "pos");
|
|
323
|
+
const kind = getAttr(tab, W_NS, "val");
|
|
324
|
+
if (pos === undefined || !INTEGER.test(pos) || kind === undefined) {
|
|
325
|
+
add({ code: "PARAGRAPH_PROPERTY_UNREADABLE", property: "tabs" });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (kind === "clear") tabs.delete(Number(pos));
|
|
329
|
+
else tabs.set(Number(pos), kind === "left" || kind === "start");
|
|
330
|
+
}
|
|
331
|
+
} else if (c.localName === "numPr") {
|
|
332
|
+
const id = findChild(c, W_NS, "numId");
|
|
333
|
+
numId = (id && getAttr(id, W_NS, "val")) ?? numId;
|
|
334
|
+
} else if (c.localName === "textDirection") {
|
|
335
|
+
if (val !== "lrTb" && val !== "tb") add({ code: "PARAGRAPH_PROPERTY", property: "textDirection" });
|
|
336
|
+
} else if (c.localName === "bidi" || c.localName === "mirrorIndents") {
|
|
337
|
+
if (onOff(c) !== false) add({ code: "PARAGRAPH_PROPERTY", property: c.localName });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
for (const u of missing(styleId)) add(u);
|
|
342
|
+
apply(defaultsPPr);
|
|
343
|
+
for (const style of chainOf(styleId)) apply(findChild(style, W_NS, "pPr"));
|
|
344
|
+
apply(pPr);
|
|
345
|
+
|
|
346
|
+
if (numId !== undefined && numId !== "0") add({ code: "PARAGRAPH_PROPERTY", property: "numPr" });
|
|
347
|
+
if (!JUSTIFICATION.has(jc)) add({ code: "PARAGRAPH_PROPERTY", property: "jc" });
|
|
348
|
+
/** @param {"left" | "right" | "firstLine"} name @returns {number} */
|
|
349
|
+
const twips = (name) => {
|
|
350
|
+
const v = ind[name];
|
|
351
|
+
if (v === undefined) return 0;
|
|
352
|
+
if (INTEGER.test(v)) return Number(v);
|
|
353
|
+
add({ code: "PARAGRAPH_PROPERTY_UNREADABLE", property: "ind" });
|
|
354
|
+
return 0;
|
|
355
|
+
};
|
|
356
|
+
return {
|
|
357
|
+
leftTwips: twips("left"),
|
|
358
|
+
rightTwips: twips("right"),
|
|
359
|
+
firstLineTwips: twips("firstLine"),
|
|
360
|
+
jc,
|
|
361
|
+
tabs: [...tabs].sort((x, y) => x[0] - y[0]).map(([posTwips, left]) => ({ posTwips, left })),
|
|
362
|
+
unknown,
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
return { run, paragraph };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The style a paragraph names (w:pPr/w:pStyle), or null.
|
|
371
|
+
* @param {XmlElement} paragraph
|
|
372
|
+
* @returns {string | null}
|
|
373
|
+
*/
|
|
374
|
+
export function paragraphStyleOf(paragraph) {
|
|
375
|
+
const pPr = findChild(paragraph, W_NS, "pPr");
|
|
376
|
+
const pStyle = pPr && findChild(pPr, W_NS, "pStyle");
|
|
377
|
+
return pStyle ? (getAttr(pStyle, W_NS, "val") ?? null) : null;
|
|
378
|
+
}
|
package/dist/io/ooxml/slots.d.ts
CHANGED
|
@@ -78,6 +78,10 @@ export type TagSite = {
|
|
|
78
78
|
name: string;
|
|
79
79
|
t: import("./xml-scan.js").XmlElement;
|
|
80
80
|
run: import("./xml-scan.js").XmlElement;
|
|
81
|
+
/**
|
|
82
|
+
* the w:p holding the run (contextProblem checked it)
|
|
83
|
+
*/
|
|
84
|
+
paragraph: import("./xml-scan.js").XmlElement;
|
|
81
85
|
};
|
|
82
86
|
/**
|
|
83
87
|
* one tag of a box's Choice copy, with its Fallback twin
|