@portone/docx-editor 0.2.0 → 0.2.1
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 +65 -0
- package/CONTRIBUTING.md +4 -0
- package/dist/docx/commentOnlyChange.d.ts +3 -1
- package/dist/docx/commentOnlyChange.js +38 -21
- package/dist/docx/comments/constants.d.ts +3 -3
- package/dist/docx/comments/constants.js +4 -3
- package/dist/docx/comments/grammar.d.ts +80 -0
- package/dist/docx/comments/grammar.js +213 -0
- package/dist/docx/comments/people.js +12 -14
- package/dist/docx/comments/reading.d.ts +5 -3
- package/dist/docx/comments/reading.js +23 -20
- package/dist/docx/comments/verifying.d.ts +44 -0
- package/dist/docx/comments/verifying.js +206 -0
- package/dist/docx/comments/writing.js +73 -46
- package/dist/docx/exportDocx.js +2 -1
- package/dist/docx/formatting/direct.js +3 -6
- package/dist/docx/headersFooters.js +8 -10
- package/dist/docx/notes.js +3 -5
- package/dist/docx/paraProps.js +51 -46
- package/dist/docx/propsXml.d.ts +20 -2
- package/dist/docx/propsXml.js +72 -32
- package/dist/docx/relationships.js +8 -3
- package/dist/docx/runProps.js +34 -34
- package/dist/docx/sdt.js +16 -14
- package/dist/docx/serializeBlock.js +1 -2
- package/dist/docx/serializeParagraph.d.ts +2 -1
- package/dist/docx/serializeParagraph.js +25 -14
- package/dist/docx/serializeTable.js +38 -24
- package/dist/docx/storyProjection.d.ts +36 -0
- package/dist/docx/storyProjection.js +23 -0
- package/dist/docx/tableFormatting/editing.js +49 -73
- package/dist/docx/tableTemplate.js +24 -3
- package/dist/docx/theme.d.ts +0 -6
- package/dist/docx/theme.js +0 -8
- package/dist/editor/clipboard/images.js +17 -3
- package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
- package/dist/editor/clipboard/inlineFormatting.js +1 -0
- package/dist/editor/commands/breakCommands.js +4 -3
- package/dist/editor/commands/canRunCommand.d.ts +2 -2
- package/dist/editor/commands/canRunCommand.js +1 -1
- package/dist/editor/commands/comments/editing.js +3 -6
- package/dist/editor/commands/historyCommands.js +2 -1
- package/dist/editor/commands/lockCommands.js +10 -1
- package/dist/editor/commands/tabCommands.js +4 -3
- package/dist/editor/createEditor.js +3 -9
- package/dist/editor/externalClipboard.js +166 -4
- package/dist/editor/insertImage.js +4 -3
- package/dist/editor/plugins/documentProtection.d.ts +3 -3
- package/dist/editor/plugins/lockedContent.d.ts +4 -4
- package/dist/editor/plugins/lockedContent.js +1 -1
- package/dist/numbering/listTemplate.js +32 -10
- package/dist/ooxml/element.d.ts +52 -0
- package/dist/ooxml/element.js +49 -0
- package/dist/ooxml/fragment.d.ts +53 -0
- package/dist/ooxml/fragment.js +76 -0
- package/dist/ooxml/image.js +4 -3
- package/dist/ooxml/names.d.ts +27 -0
- package/dist/ooxml/names.js +29 -0
- package/dist/ooxml/precedence.d.ts +31 -0
- package/dist/ooxml/precedence.js +51 -0
- package/dist/ooxml/xml.d.ts +31 -0
- package/dist/ooxml/xml.js +27 -0
- package/dist/page/blockKinds.d.ts +42 -0
- package/dist/page/blockKinds.js +0 -0
- package/dist/page/measureBlocks.d.ts +14 -1
- package/dist/page/measureBlocks.js +31 -11
- package/dist/page/pageDecorations.d.ts +16 -0
- package/dist/page/pageDecorations.js +1 -0
- package/dist/page/pageLayout.d.ts +8 -43
- package/dist/page/pageLayout.js +28 -51
- package/dist/page/tableMeasurements.d.ts +10 -2
- package/dist/page/tableMeasurements.js +20 -16
- package/dist/page/usePageLayout.d.ts +15 -0
- package/dist/page/usePageLayout.js +30 -2
- package/dist/schema/attrRoles.d.ts +35 -0
- package/dist/schema/attrRoles.js +106 -0
- package/dist/schema/docxSchema.d.ts +6 -0
- package/dist/schema/docxSchema.js +212 -101
- package/dist/schema/editGuard.d.ts +99 -0
- package/dist/schema/editGuard.js +43 -0
- package/dist/schema/guards.d.ts +68 -0
- package/dist/schema/guards.js +82 -0
- package/dist/schema/locks.d.ts +8 -35
- package/dist/schema/locks.js +29 -64
- package/dist/schema/preservedGuards.d.ts +30 -0
- package/dist/schema/preservedGuards.js +50 -0
- package/dist/schema/protection.d.ts +10 -3
- package/dist/schema/protection.js +2 -1
- package/dist/schema/sourceEquality.d.ts +27 -0
- package/dist/schema/sourceEquality.js +65 -0
- package/dist/table/cellFormatting.js +2 -1
- package/dist/table/commands.d.ts +2 -2
- package/dist/table/commands.js +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/merge.d.ts +2 -2
- package/dist/table/merge.js +1 -1
- package/package.json +4 -1
- package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
- package/dist/editor/plugins/bookmarkProtection.js +0 -28
- package/dist/editor/plugins/noteProtection.d.ts +0 -3
- package/dist/editor/plugins/noteProtection.js +0 -25
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/ooxml/element.ts
|
|
2
|
+
import { NAMESPACES, wName } from "./names.js";
|
|
3
|
+
import { escapeXml, localPart } from "./xml.js";
|
|
4
|
+
function attrsText(attrs) {
|
|
5
|
+
return attrs.map(([name, value]) => `${name}="${escapeXml(value)}"`).join(" ");
|
|
6
|
+
}
|
|
7
|
+
function elementXml(name, attrs, children = []) {
|
|
8
|
+
const text = attrsText(attrs);
|
|
9
|
+
const open = text === "" ? `<${name}` : `<${name} ${text}`;
|
|
10
|
+
if (children.length === 0) return `${open}/>`;
|
|
11
|
+
return `${open}>${children.join("")}</${name}>`;
|
|
12
|
+
}
|
|
13
|
+
function xmlnsAttr(prefix) {
|
|
14
|
+
return [`xmlns:${prefix}`, NAMESPACES[prefix]];
|
|
15
|
+
}
|
|
16
|
+
function withAttr(attrs, name, value) {
|
|
17
|
+
if (value === null) return withoutAttrs(attrs, [name]);
|
|
18
|
+
const at = attrs.findIndex(([attr]) => localPart(attr) === name);
|
|
19
|
+
if (at === -1) return [...attrs, [wName(name), value]];
|
|
20
|
+
return attrs.map(
|
|
21
|
+
(attr, index) => index === at ? [attr[0], value] : attr
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
function withoutAttrs(attrs, localNames) {
|
|
25
|
+
return attrs.filter(([name]) => !localNames.includes(localPart(name)));
|
|
26
|
+
}
|
|
27
|
+
function attrValue(attrs, localName) {
|
|
28
|
+
return attrs.find(([name]) => localPart(name) === localName)?.[1] ?? null;
|
|
29
|
+
}
|
|
30
|
+
function openTagXml(name, rawAttrs) {
|
|
31
|
+
return rawAttrs ? `<${name} ${rawAttrs}>` : `<${name}>`;
|
|
32
|
+
}
|
|
33
|
+
function emptyTagXml(name, rawAttrs) {
|
|
34
|
+
return rawAttrs ? `<${name} ${rawAttrs}/>` : `<${name}/>`;
|
|
35
|
+
}
|
|
36
|
+
function attrPairs(el) {
|
|
37
|
+
return el ? Array.from(el.attributes, (attr) => [attr.name, attr.value]) : [];
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
attrPairs,
|
|
41
|
+
attrValue,
|
|
42
|
+
attrsText,
|
|
43
|
+
elementXml,
|
|
44
|
+
emptyTagXml,
|
|
45
|
+
openTagXml,
|
|
46
|
+
withAttr,
|
|
47
|
+
withoutAttrs,
|
|
48
|
+
xmlnsAttr
|
|
49
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate a raw OOXML string passes through on its way from the DOM into a node or mark attr.
|
|
3
|
+
*
|
|
4
|
+
* A preserved fragment goes back out the way it came, spliced into a slot the writer opens and
|
|
5
|
+
* closes around it (`docx/serializeParagraph`), so a fragment that closes that slot itself, opens
|
|
6
|
+
* a sibling beside it, or lays text down beside what it holds writes something the editor never
|
|
7
|
+
* modelled into the exported file. What stands inside the element it carries is that element's own
|
|
8
|
+
* and travels with it. Reading one back is the only place such a string can arrive from outside,
|
|
9
|
+
* so every attr that carries one is read through here and a fragment that does not hold its shape
|
|
10
|
+
* is turned down rather than corrected.
|
|
11
|
+
*
|
|
12
|
+
* A refusal is not an error: `getAttrs` answers `false` with it, which drops the parse rule and
|
|
13
|
+
* leaves ProseMirror to settle the content one level plainer. Demotion, not contamination.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* What a raw XML string has to look like to be let into the attr that carries it.
|
|
17
|
+
*
|
|
18
|
+
* `element` is a whole element: a properties fragment, a drawing, an annotation reference.
|
|
19
|
+
* `attributes` is what stood inside an opening tag, as `attrString` writes it.
|
|
20
|
+
* `openTag` is an opening tag with everything it wrapped cut away, which the writer puts back by
|
|
21
|
+
* appending the closing text `closedBy` names. `head` is what may still stand between the two:
|
|
22
|
+
* the property elements the tag carries ahead of its content, by local name.
|
|
23
|
+
*/
|
|
24
|
+
export type RawXmlShape = {
|
|
25
|
+
kind: "element";
|
|
26
|
+
names: readonly string[] | "any";
|
|
27
|
+
} | {
|
|
28
|
+
kind: "attributes";
|
|
29
|
+
} | {
|
|
30
|
+
kind: "openTag";
|
|
31
|
+
name: string;
|
|
32
|
+
closedBy: string;
|
|
33
|
+
head: readonly string[];
|
|
34
|
+
};
|
|
35
|
+
/** A single element by local name, rejecting an explicitly foreign namespace */
|
|
36
|
+
export declare function ELEMENT(...names: string[]): RawXmlShape;
|
|
37
|
+
/**
|
|
38
|
+
* A single element of any name, in any namespace.
|
|
39
|
+
*
|
|
40
|
+
* What import could not model keeps its own XML (`rawInline`, `rawBlock`), and that is a
|
|
41
|
+
* `m:oMathPara`, a `mc:AlternateContent`, a `w:ins`, a bookmark: naming the ones allowed would
|
|
42
|
+
* turn a document the editor reads today into a demoted one.
|
|
43
|
+
*/
|
|
44
|
+
export declare const ANY_ELEMENT: RawXmlShape;
|
|
45
|
+
/** The attributes of an opening tag, with neither the tag nor anything it held */
|
|
46
|
+
export declare const ATTRIBUTES: RawXmlShape;
|
|
47
|
+
/**
|
|
48
|
+
* The value if it holds the shape, null if there was none, `false` if it does not.
|
|
49
|
+
*
|
|
50
|
+
* The three answers are what `getAttrs` needs: a value to carry, an absent attr, and a rule to
|
|
51
|
+
* give up on.
|
|
52
|
+
*/
|
|
53
|
+
export declare function acceptRawXml(shape: RawXmlShape, value: string | null): string | null | false;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/ooxml/fragment.ts
|
|
2
|
+
import {
|
|
3
|
+
elementChildren,
|
|
4
|
+
namespaceDecls,
|
|
5
|
+
parseXml,
|
|
6
|
+
RESERVED_PREFIXES,
|
|
7
|
+
W_NS
|
|
8
|
+
} from "./xml.js";
|
|
9
|
+
function ELEMENT(...names) {
|
|
10
|
+
return { kind: "element", names };
|
|
11
|
+
}
|
|
12
|
+
var ANY_ELEMENT = { kind: "element", names: "any" };
|
|
13
|
+
var ATTRIBUTES = { kind: "attributes" };
|
|
14
|
+
function loneElement(xml) {
|
|
15
|
+
let root;
|
|
16
|
+
try {
|
|
17
|
+
root = parseXml(`<x ${namespaceDecls(xml)}>${xml}</x>`).documentElement;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (root.childNodes.length !== 1) return null;
|
|
22
|
+
const el = elementChildren(root)[0];
|
|
23
|
+
return el === void 0 || rebindsReservedPrefix(el) ? null : el;
|
|
24
|
+
}
|
|
25
|
+
function isNamed(el, names) {
|
|
26
|
+
if (!names.includes(el.localName)) return false;
|
|
27
|
+
const declaration = el.prefix === null ? "xmlns" : `xmlns:${el.prefix}`;
|
|
28
|
+
for (let scope = el; scope.parentElement !== null; scope = scope.parentElement) {
|
|
29
|
+
if (scope.hasAttribute(declaration)) return el.namespaceURI === W_NS;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
function holdsHeadAlone(el, shape) {
|
|
34
|
+
const closing = loneElement(`<w:${shape.name}>${shape.closedBy}`);
|
|
35
|
+
if (closing === null) return false;
|
|
36
|
+
const children = elementChildren(el);
|
|
37
|
+
const own = children.slice(
|
|
38
|
+
0,
|
|
39
|
+
children.length - elementChildren(closing).length
|
|
40
|
+
);
|
|
41
|
+
return el.childNodes.length === children.length && own.every((child) => isNamed(child, shape.head));
|
|
42
|
+
}
|
|
43
|
+
function rebindsReservedPrefix(el) {
|
|
44
|
+
return Array.from(el.attributes).some((attr) => {
|
|
45
|
+
if (attr.prefix !== "xmlns") return false;
|
|
46
|
+
const reserved = RESERVED_PREFIXES.get(attr.localName);
|
|
47
|
+
return reserved !== void 0 && reserved !== attr.value;
|
|
48
|
+
}) || elementChildren(el).some(rebindsReservedPrefix);
|
|
49
|
+
}
|
|
50
|
+
function holdsShape(shape, value) {
|
|
51
|
+
switch (shape.kind) {
|
|
52
|
+
case "element": {
|
|
53
|
+
const el = loneElement(value);
|
|
54
|
+
if (el === null) return false;
|
|
55
|
+
return shape.names === "any" || isNamed(el, shape.names);
|
|
56
|
+
}
|
|
57
|
+
case "attributes": {
|
|
58
|
+
const el = loneElement(`<y ${value}></y>`);
|
|
59
|
+
return el !== null && el.childNodes.length === 0;
|
|
60
|
+
}
|
|
61
|
+
case "openTag": {
|
|
62
|
+
const el = loneElement(value + shape.closedBy);
|
|
63
|
+
return el !== null && holdsHeadAlone(el, shape);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function acceptRawXml(shape, value) {
|
|
68
|
+
if (value === null) return null;
|
|
69
|
+
return holdsShape(shape, value) ? value : false;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
ANY_ELEMENT,
|
|
73
|
+
ATTRIBUTES,
|
|
74
|
+
ELEMENT,
|
|
75
|
+
acceptRawXml
|
|
76
|
+
};
|
package/dist/ooxml/image.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/ooxml/image.ts
|
|
2
|
+
import { NAMESPACES } from "./names.js";
|
|
2
3
|
import { childByLocalName, escapeXml, R_NS } from "./xml.js";
|
|
3
4
|
var EMU_PER_PX = 9525;
|
|
4
5
|
function emuToPx(emu) {
|
|
@@ -42,7 +43,7 @@ function toImageExtent(value) {
|
|
|
42
43
|
const cy = positiveInt(value.cy);
|
|
43
44
|
return cx === null || cy === null ? null : { cx, cy };
|
|
44
45
|
}
|
|
45
|
-
var PICTURE_URI =
|
|
46
|
+
var PICTURE_URI = NAMESPACES.pic;
|
|
46
47
|
function childOf(el, name) {
|
|
47
48
|
return el ? childByLocalName(el, name) : null;
|
|
48
49
|
}
|
|
@@ -82,8 +83,8 @@ function withExtent(xml, extent) {
|
|
|
82
83
|
const size = `cx="${extent.cx}" cy="${extent.cy}"`;
|
|
83
84
|
return xml.replace(EXTENT_TAG, (_match, prefix) => `<${prefix ?? ""}extent ${size}/>`).replace(EXT_TAG, (_match, prefix) => `<${prefix ?? ""}ext ${size}/>`);
|
|
84
85
|
}
|
|
85
|
-
var WP_NS =
|
|
86
|
-
var A_NS =
|
|
86
|
+
var WP_NS = NAMESPACES.wp;
|
|
87
|
+
var A_NS = NAMESPACES.a;
|
|
87
88
|
function imageDrawingXml(image) {
|
|
88
89
|
const size = `cx="${image.extent.cx}" cy="${image.extent.cy}"`;
|
|
89
90
|
const name = `Picture ${image.docPrId}`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The namespace prefixes the writer spells out, and the namespace each one stands for.
|
|
3
|
+
*
|
|
4
|
+
* Reading is another matter: a document may bind these prefixes to names of its own, so an element
|
|
5
|
+
* that arrived in the file is looked up by its local part. What this package writes is settled
|
|
6
|
+
* here, and the part a fragment is spliced into declares the same binding, so the prefix is
|
|
7
|
+
* decided once rather than at every call site.
|
|
8
|
+
*/
|
|
9
|
+
/** The prefix every WordprocessingML element and attribute this package writes carries */
|
|
10
|
+
export declare const W_PREFIX = "w";
|
|
11
|
+
export declare const NAMESPACES: {
|
|
12
|
+
readonly w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
13
|
+
readonly r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
14
|
+
readonly w14: "http://schemas.microsoft.com/office/word/2010/wordml";
|
|
15
|
+
readonly w15: "http://schemas.microsoft.com/office/word/2012/wordml";
|
|
16
|
+
readonly mc: "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
|
17
|
+
readonly wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
|
|
18
|
+
readonly a: "http://schemas.openxmlformats.org/drawingml/2006/main";
|
|
19
|
+
readonly pic: "http://schemas.openxmlformats.org/drawingml/2006/picture";
|
|
20
|
+
};
|
|
21
|
+
export type KnownPrefix = keyof typeof NAMESPACES;
|
|
22
|
+
/** The name `local` goes out under (`qualify("w15", "commentEx")` -> `w15:commentEx`) */
|
|
23
|
+
export declare function qualify(prefix: KnownPrefix, local: string): string;
|
|
24
|
+
/** `wName("val")` -> `w:val`. The one place the written WordprocessingML prefix is decided */
|
|
25
|
+
export declare function wName(local: string): string;
|
|
26
|
+
/** The declaration a part root carries for one prefix, ready to write inside an opening tag */
|
|
27
|
+
export declare function xmlnsDecl(prefix: KnownPrefix): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/ooxml/names.ts
|
|
2
|
+
import { R_NS, W_NS } from "./xml.js";
|
|
3
|
+
var W_PREFIX = "w";
|
|
4
|
+
var NAMESPACES = {
|
|
5
|
+
w: W_NS,
|
|
6
|
+
r: R_NS,
|
|
7
|
+
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
|
|
8
|
+
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
|
|
9
|
+
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
|
10
|
+
wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
|
|
11
|
+
a: "http://schemas.openxmlformats.org/drawingml/2006/main",
|
|
12
|
+
pic: "http://schemas.openxmlformats.org/drawingml/2006/picture"
|
|
13
|
+
};
|
|
14
|
+
function qualify(prefix, local) {
|
|
15
|
+
return `${prefix}:${local}`;
|
|
16
|
+
}
|
|
17
|
+
function wName(local) {
|
|
18
|
+
return qualify(W_PREFIX, local);
|
|
19
|
+
}
|
|
20
|
+
function xmlnsDecl(prefix) {
|
|
21
|
+
return `xmlns:${prefix}="${NAMESPACES[prefix]}"`;
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
NAMESPACES,
|
|
25
|
+
W_PREFIX,
|
|
26
|
+
qualify,
|
|
27
|
+
wName,
|
|
28
|
+
xmlnsDecl
|
|
29
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which attribute beats which when an element writes down both.
|
|
3
|
+
*
|
|
4
|
+
* OOXML records the same setting twice over in several places: a font slot beside the theme
|
|
5
|
+
* reference that overrules it, a color beside the theme color, an indent in twips beside the same
|
|
6
|
+
* indent counted in characters. Writing one of a pair while leaving the other standing means
|
|
7
|
+
* writing a value Word will not show, so the one that would win is dropped as the other is written.
|
|
8
|
+
*
|
|
9
|
+
* The table is keyed by the local names of the owning element and the attribute together, because
|
|
10
|
+
* neither is enough on its own: `color` is an attribute of `w:shd` but an element of its own whose
|
|
11
|
+
* value sits in `w:val`, and `left` is an attribute of `w:ind` but an element of `w:tcMar`.
|
|
12
|
+
*/
|
|
13
|
+
import { type XmlAttr } from "./element";
|
|
14
|
+
/**
|
|
15
|
+
* The attributes that override the named one when both are there, so writing the named one drops
|
|
16
|
+
* them (§17.3.1.12 ind, §17.3.2.26 rFonts, §17.3.2.6 color, §17.3.5 shd, §17.3.1.33 spacing).
|
|
17
|
+
*
|
|
18
|
+
* The border sides are the children of `w:tcBorders`, each of them a CT_Border that records a
|
|
19
|
+
* color of its own.
|
|
20
|
+
*/
|
|
21
|
+
export declare const OVERRIDING_ATTRS: Readonly<Record<`${string}/${string}`, readonly string[]>>;
|
|
22
|
+
/** What writing `element`'s `name` drops along with it. Empty for a pair nothing overrides */
|
|
23
|
+
export declare function overridingAttrs(element: string, name: string): readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* `withAttr` with the attributes the new value would be fighting with dropped as it is written.
|
|
26
|
+
*
|
|
27
|
+
* This is how an attribute is meant to be written, so that a writer added later cannot forget the
|
|
28
|
+
* drop. A caller that means to leave an overriding attribute standing says so by reaching for
|
|
29
|
+
* `withAttr` instead.
|
|
30
|
+
*/
|
|
31
|
+
export declare function setAttr(attrs: readonly XmlAttr[], element: string, name: string, value: string | null): XmlAttr[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/ooxml/precedence.ts
|
|
2
|
+
import { withAttr, withoutAttrs } from "./element.js";
|
|
3
|
+
var THEME_COLOR = [
|
|
4
|
+
"themeColor",
|
|
5
|
+
"themeTint",
|
|
6
|
+
"themeShade"
|
|
7
|
+
];
|
|
8
|
+
var NO_OVERRIDES = [];
|
|
9
|
+
var OVERRIDING_ATTRS = {
|
|
10
|
+
"rFonts/ascii": ["asciiTheme"],
|
|
11
|
+
"rFonts/hAnsi": ["hAnsiTheme"],
|
|
12
|
+
"rFonts/eastAsia": ["eastAsiaTheme"],
|
|
13
|
+
// This is the slot whose capitalization varies from document to document
|
|
14
|
+
"rFonts/cs": ["cstheme", "csTheme"],
|
|
15
|
+
"color/val": THEME_COLOR,
|
|
16
|
+
"shd/color": THEME_COLOR,
|
|
17
|
+
"shd/fill": ["themeFill", "themeFillTint", "themeFillShade"],
|
|
18
|
+
"ind/left": ["leftChars"],
|
|
19
|
+
"ind/start": ["startChars"],
|
|
20
|
+
"ind/right": ["rightChars"],
|
|
21
|
+
"ind/end": ["endChars"],
|
|
22
|
+
"ind/hanging": ["hangingChars"],
|
|
23
|
+
"ind/firstLine": ["firstLineChars"],
|
|
24
|
+
"spacing/before": ["beforeLines"],
|
|
25
|
+
"spacing/after": ["afterLines"],
|
|
26
|
+
"top/color": THEME_COLOR,
|
|
27
|
+
"start/color": THEME_COLOR,
|
|
28
|
+
"left/color": THEME_COLOR,
|
|
29
|
+
"bottom/color": THEME_COLOR,
|
|
30
|
+
"end/color": THEME_COLOR,
|
|
31
|
+
"right/color": THEME_COLOR,
|
|
32
|
+
"insideH/color": THEME_COLOR,
|
|
33
|
+
"insideV/color": THEME_COLOR,
|
|
34
|
+
"tl2br/color": THEME_COLOR,
|
|
35
|
+
"tr2bl/color": THEME_COLOR
|
|
36
|
+
};
|
|
37
|
+
function overridingAttrs(element, name) {
|
|
38
|
+
return OVERRIDING_ATTRS[`${element}/${name}`] ?? NO_OVERRIDES;
|
|
39
|
+
}
|
|
40
|
+
function setAttr(attrs, element, name, value) {
|
|
41
|
+
return withAttr(
|
|
42
|
+
withoutAttrs(attrs, overridingAttrs(element, name)),
|
|
43
|
+
name,
|
|
44
|
+
value
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
OVERRIDING_ATTRS,
|
|
49
|
+
overridingAttrs,
|
|
50
|
+
setAttr
|
|
51
|
+
};
|
package/dist/ooxml/xml.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export declare const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/
|
|
|
5
5
|
* every relationship type name is built on
|
|
6
6
|
*/
|
|
7
7
|
export declare const R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
8
|
+
/**
|
|
9
|
+
* The prefixes whose meaning the editor depends on, and the namespace each one has to carry.
|
|
10
|
+
*
|
|
11
|
+
* Every element the writer spells out is a `w:` one, and the relationship a link or an image points
|
|
12
|
+
* at is named by `r:id` or `r:embed`, so a fragment that bound either prefix elsewhere would leave
|
|
13
|
+
* the writer's own markup meaning something else.
|
|
14
|
+
*/
|
|
15
|
+
export declare const RESERVED_PREFIXES: ReadonlyMap<string, string>;
|
|
8
16
|
/** The name with its namespace prefix stripped off (`w:ascii` -> `ascii`) */
|
|
9
17
|
export declare function localPart(name: string): string;
|
|
10
18
|
/**
|
|
@@ -22,10 +30,33 @@ export declare function decodeUtf8(bytes: Uint8Array): {
|
|
|
22
30
|
};
|
|
23
31
|
export declare function encodeUtf8(text: string, withBom: boolean): Uint8Array;
|
|
24
32
|
export declare function parseXml(source: string): Document;
|
|
33
|
+
/**
|
|
34
|
+
* Gathers the namespace prefixes used in the fragment and declares them.
|
|
35
|
+
* Only `w` carries real meaning; the rest are placeholders that keep the parser from stopping.
|
|
36
|
+
* All we read are element names and `w:` attributes, so placeholders still let the values be read as they are.
|
|
37
|
+
*
|
|
38
|
+
* An attribute standing at the very start of the string counts too, since a fragment may be an
|
|
39
|
+
* attribute list of its own (`attrString`) rather than an element.
|
|
40
|
+
*/
|
|
41
|
+
export declare function namespaceDecls(xml: string): string;
|
|
25
42
|
export declare function elementChildren(el: Element): Element[];
|
|
43
|
+
/**
|
|
44
|
+
* The value of the attribute with this local name, whatever prefix it was written under.
|
|
45
|
+
*
|
|
46
|
+
* A document is free to bind the WordprocessingML namespace to a prefix of its own, so an
|
|
47
|
+
* attribute is looked up by the name it carries rather than by the spelling a producer chose.
|
|
48
|
+
*/
|
|
49
|
+
export declare function attributeByLocalName(el: Element, localName: string): string | null;
|
|
26
50
|
export declare function childByLocalName(el: Element, name: string): Element | null;
|
|
27
51
|
/** Turns an opening tag's attributes into an `a="b" c="d"` string */
|
|
28
52
|
export declare function attrString(el: Element): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Whether this node is an element.
|
|
55
|
+
*
|
|
56
|
+
* `Node` is a global a server is asked to install (`site/content/docs/core.mdx`); `Element` is
|
|
57
|
+
* not, so nothing here may reach for it at run time.
|
|
58
|
+
*/
|
|
59
|
+
export declare function isElement(node: Node): node is Element;
|
|
29
60
|
/**
|
|
30
61
|
* Turns a single element back into an XML string.
|
|
31
62
|
*
|
package/dist/ooxml/xml.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import { DocxImportError } from "./errors.js";
|
|
3
3
|
var W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
4
4
|
var R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
5
|
+
var RESERVED_PREFIXES = /* @__PURE__ */ new Map([
|
|
6
|
+
["w", W_NS],
|
|
7
|
+
["r", R_NS]
|
|
8
|
+
]);
|
|
5
9
|
function localPart(name) {
|
|
6
10
|
const colon = name.indexOf(":");
|
|
7
11
|
return colon === -1 ? name : name.slice(colon + 1);
|
|
@@ -42,9 +46,28 @@ function parseXml(source) {
|
|
|
42
46
|
}
|
|
43
47
|
return doc;
|
|
44
48
|
}
|
|
49
|
+
function namespaceDecls(xml) {
|
|
50
|
+
const prefixes = /* @__PURE__ */ new Set(["w"]);
|
|
51
|
+
for (const [, prefix] of xml.matchAll(/<\/?([A-Za-z_][\w.-]*):/g)) {
|
|
52
|
+
prefixes.add(prefix);
|
|
53
|
+
}
|
|
54
|
+
for (const [, prefix] of xml.matchAll(
|
|
55
|
+
/(?:^|[\s"'])([A-Za-z_][\w.-]*):[\w.-]+=/g
|
|
56
|
+
)) {
|
|
57
|
+
prefixes.add(prefix);
|
|
58
|
+
}
|
|
59
|
+
prefixes.delete("xml");
|
|
60
|
+
prefixes.delete("xmlns");
|
|
61
|
+
return Array.from(prefixes).map(
|
|
62
|
+
(prefix) => `xmlns:${prefix}="${prefix === "w" ? W_NS : `urn:docx-editor:${prefix}`}"`
|
|
63
|
+
).join(" ");
|
|
64
|
+
}
|
|
45
65
|
function elementChildren(el) {
|
|
46
66
|
return Array.from(el.children);
|
|
47
67
|
}
|
|
68
|
+
function attributeByLocalName(el, localName) {
|
|
69
|
+
return Array.from(el.attributes).find((entry) => entry.localName === localName)?.value ?? null;
|
|
70
|
+
}
|
|
48
71
|
function childByLocalName(el, name) {
|
|
49
72
|
return elementChildren(el).find((child) => child.localName === name) ?? null;
|
|
50
73
|
}
|
|
@@ -75,15 +98,19 @@ function serializeXml(el) {
|
|
|
75
98
|
return inner ? `${open}>${inner}</${el.nodeName}>` : `${open}/>`;
|
|
76
99
|
}
|
|
77
100
|
export {
|
|
101
|
+
RESERVED_PREFIXES,
|
|
78
102
|
R_NS,
|
|
79
103
|
W_NS,
|
|
80
104
|
attrString,
|
|
105
|
+
attributeByLocalName,
|
|
81
106
|
childByLocalName,
|
|
82
107
|
decodeUtf8,
|
|
83
108
|
elementChildren,
|
|
84
109
|
encodeUtf8,
|
|
85
110
|
escapeXml,
|
|
111
|
+
isElement,
|
|
86
112
|
localPart,
|
|
113
|
+
namespaceDecls,
|
|
87
114
|
parseXml,
|
|
88
115
|
serializeXml
|
|
89
116
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the page engine knows about a block, whatever shape of block it is.
|
|
3
|
+
*
|
|
4
|
+
* A measurer says where a block may be parted and how much of it has to fit on the page it
|
|
5
|
+
* starts on; the layout (`page/pageLayout`) answers with the places it decided to part it at.
|
|
6
|
+
* Neither side names a paragraph or a table.
|
|
7
|
+
*/
|
|
8
|
+
/** A place inside a block where the next page may, or must, start */
|
|
9
|
+
export interface BreakCandidate {
|
|
10
|
+
/**
|
|
11
|
+
* The document position the continued piece starts at: a page `br` for a paragraph, a row for a
|
|
12
|
+
* table. A position maps through an edit, which is why the marks are kept by position and not
|
|
13
|
+
* by ordinal (see `page/pageDecorations`)
|
|
14
|
+
*/
|
|
15
|
+
at: number;
|
|
16
|
+
/** The top of the continued piece, measured from the block's natural top with no space in it */
|
|
17
|
+
offset: number;
|
|
18
|
+
/** A forced candidate always cuts; an optional one cuts only when the piece after it would overflow */
|
|
19
|
+
forced: boolean;
|
|
20
|
+
/** Height the continued piece carries onto the next page ahead of its own content (a repeated header) */
|
|
21
|
+
repeatHeight: number;
|
|
22
|
+
}
|
|
23
|
+
/** One body block as drawn on screen */
|
|
24
|
+
export interface MeasuredBlock {
|
|
25
|
+
/** The position where this block starts in the document */
|
|
26
|
+
pos: number;
|
|
27
|
+
/** The height that naturally opens up between the previous block and this one */
|
|
28
|
+
gap: number;
|
|
29
|
+
height: number;
|
|
30
|
+
/** The document records that a new page starts at this block */
|
|
31
|
+
breakBefore: boolean;
|
|
32
|
+
/** The block holds a break it cannot open a space at (inside a cell), so the next block starts a page */
|
|
33
|
+
breakAfter: boolean;
|
|
34
|
+
candidates: readonly BreakCandidate[];
|
|
35
|
+
/** The smallest piece that has to fit on the page the block starts on */
|
|
36
|
+
minFirstPiece: number;
|
|
37
|
+
}
|
|
38
|
+
/** A page cut the layout decided on: the space opened before the continued piece */
|
|
39
|
+
export interface PageCut {
|
|
40
|
+
at: number;
|
|
41
|
+
height: number;
|
|
42
|
+
}
|
|
File without changes
|
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
* Measures the sheet as drawn on screen: the height of each body block, the gap above it, and
|
|
3
3
|
* where the page breaks inside it stand, with nothing computed line by line.
|
|
4
4
|
*
|
|
5
|
+
* Every place a block may be parted at is read as a break candidate (`page/blockKinds`), whatever
|
|
6
|
+
* shape of block it is: a page break for a paragraph, a safe row boundary for a table.
|
|
5
7
|
* The engine's own marks are taken back off as they are read, so a block already pushed and a
|
|
6
8
|
* break already given its space read as they would with neither applied. A measurement that read
|
|
7
9
|
* them in would add to what is already there, and the layout would creep on every pass.
|
|
8
10
|
* Where there is no layout (in tests) everything is 0, so the result is a single page.
|
|
9
11
|
*/
|
|
10
12
|
import type { EditorView } from "prosemirror-view";
|
|
11
|
-
import type { MeasuredBlock } from "./
|
|
13
|
+
import type { MeasuredBlock } from "./blockKinds";
|
|
14
|
+
/** What a table continuation still needs from the measurement to be drawn */
|
|
15
|
+
export interface TableHeaderProjection {
|
|
16
|
+
headerRows: readonly number[];
|
|
17
|
+
headerSignature: string;
|
|
18
|
+
columns: number;
|
|
19
|
+
}
|
|
12
20
|
/**
|
|
13
21
|
* The measurements taken in order to draw the page overlay. Positions are relative to
|
|
14
22
|
* the overlay box
|
|
@@ -21,5 +29,10 @@ export interface SheetMeasure {
|
|
|
21
29
|
contentTop: number;
|
|
22
30
|
contentBottom: number;
|
|
23
31
|
blocks: MeasuredBlock[];
|
|
32
|
+
/**
|
|
33
|
+
* The header facts of every table block, by the table's position, for the adapter that turns
|
|
34
|
+
* cuts back into table continuations (`page/usePageLayout`)
|
|
35
|
+
*/
|
|
36
|
+
tables: ReadonlyMap<number, TableHeaderProjection>;
|
|
24
37
|
}
|
|
25
38
|
export declare function measureSheet(view: EditorView, layer: HTMLElement): SheetMeasure;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/page/measureBlocks.ts
|
|
2
2
|
import { editorAttributes } from "../styles/classNames.js";
|
|
3
|
+
import { pageBreaksIn } from "./pageDecorations.js";
|
|
3
4
|
import { measureTable } from "./tableMeasurements.js";
|
|
4
5
|
var PAGE_BREAK_BR = `br[${editorAttributes.breakType}="page"]`;
|
|
5
6
|
var BREAK_SPACE = `[${editorAttributes.pageBreakSpace}]`;
|
|
@@ -37,34 +38,52 @@ function measureSheet(view, layer) {
|
|
|
37
38
|
const contentBottom = pixels(style.paddingBottom);
|
|
38
39
|
const sheetY = (viewportY) => (viewportY - sheetRect.top) / scale;
|
|
39
40
|
const blocks = [];
|
|
41
|
+
const tables = /* @__PURE__ */ new Map();
|
|
40
42
|
let previousBottom = contentTop;
|
|
41
43
|
let applied = 0;
|
|
42
|
-
let breakAfterPrevious = false;
|
|
43
44
|
for (const { pos, dom } of drawnBlocks(view)) {
|
|
44
45
|
const node = view.state.doc.nodeAt(pos);
|
|
45
46
|
const rect = dom.getBoundingClientRect();
|
|
46
47
|
applied += appliedPush(dom);
|
|
47
48
|
const top = sheetY(rect.top) - applied;
|
|
48
49
|
const measuredTable = node ? measureTable(view, node, pos, dom, scale) : null;
|
|
49
|
-
const breaks = [];
|
|
50
|
+
const breaks = node ? pageBreaksIn(node, pos) : [];
|
|
50
51
|
const spaces = Array.from(dom.querySelectorAll(BREAK_SPACE));
|
|
51
|
-
|
|
52
|
+
const forced = [];
|
|
53
|
+
spaces.forEach((space, index) => {
|
|
52
54
|
const box = space.getBoundingClientRect();
|
|
53
|
-
breaks.
|
|
55
|
+
const found = breaks.at(index);
|
|
56
|
+
if (found) {
|
|
57
|
+
forced.push({
|
|
58
|
+
at: found.at,
|
|
59
|
+
offset: sheetY(box.top) - applied - top,
|
|
60
|
+
forced: true,
|
|
61
|
+
repeatHeight: 0
|
|
62
|
+
});
|
|
63
|
+
}
|
|
54
64
|
applied += box.height / scale;
|
|
55
|
-
}
|
|
65
|
+
});
|
|
56
66
|
applied += measuredTable?.appliedHeight ?? 0;
|
|
57
67
|
const bottom = sheetY(rect.bottom) - applied;
|
|
68
|
+
const height = bottom - top;
|
|
69
|
+
if (measuredTable) {
|
|
70
|
+
tables.set(pos, {
|
|
71
|
+
headerRows: measuredTable.headerRows,
|
|
72
|
+
headerSignature: measuredTable.headerSignature,
|
|
73
|
+
columns: measuredTable.columns
|
|
74
|
+
});
|
|
75
|
+
}
|
|
58
76
|
blocks.push({
|
|
59
77
|
pos,
|
|
60
78
|
gap: top - previousBottom,
|
|
61
|
-
height
|
|
62
|
-
breakBefore:
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
height,
|
|
80
|
+
breakBefore: dom.hasAttribute(editorAttributes.pageBreakBefore),
|
|
81
|
+
breakAfter: breakWithoutSpace(dom, spaces.length),
|
|
82
|
+
// A space is never opened inside a table, so at most one of the two lists holds anything
|
|
83
|
+
candidates: [...forced, ...measuredTable?.candidates ?? []],
|
|
84
|
+
minFirstPiece: measuredTable?.minFirstPiece ?? forced[0]?.offset ?? height
|
|
65
85
|
});
|
|
66
86
|
previousBottom = bottom;
|
|
67
|
-
breakAfterPrevious = breakWithoutSpace(dom, spaces.length);
|
|
68
87
|
}
|
|
69
88
|
return {
|
|
70
89
|
left: (sheetRect.left - layerRect.left) / scale,
|
|
@@ -72,7 +91,8 @@ function measureSheet(view, layer) {
|
|
|
72
91
|
width: sheetRect.width / scale,
|
|
73
92
|
contentTop,
|
|
74
93
|
contentBottom,
|
|
75
|
-
blocks
|
|
94
|
+
blocks,
|
|
95
|
+
tables
|
|
76
96
|
};
|
|
77
97
|
}
|
|
78
98
|
export {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* The document model is left untouched, so no trace of them is left in
|
|
6
6
|
* the exported XML or in the edit history.
|
|
7
7
|
*/
|
|
8
|
+
import type { Node as PMNode } from "prosemirror-model";
|
|
8
9
|
import { Plugin } from "prosemirror-state";
|
|
9
10
|
import { DecorationSet, type EditorView } from "prosemirror-view";
|
|
10
11
|
import type { BlockPush, BreakSpace, TableContinuation } from "./pageLayout";
|
|
@@ -26,6 +27,21 @@ interface PageMarks {
|
|
|
26
27
|
tableContinuations: readonly TableContinuation[];
|
|
27
28
|
decorations: DecorationSet;
|
|
28
29
|
}
|
|
30
|
+
/** One page break inside a block, where it stands and how much room the `br` itself takes */
|
|
31
|
+
interface BreakAt {
|
|
32
|
+
at: number;
|
|
33
|
+
size: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every page break inside this block, in document order.
|
|
37
|
+
* A space is put on the breaks of a top-level paragraph alone: inside a table cell it would grow
|
|
38
|
+
* the cell rather than the page, so a break there is left to the whole-block rule in
|
|
39
|
+
* `page/measureBlocks`, and none is found here.
|
|
40
|
+
*
|
|
41
|
+
* The measurement reads the same list to pair each space element it finds with the break it
|
|
42
|
+
* belongs to (`page/measureBlocks`).
|
|
43
|
+
*/
|
|
44
|
+
export declare function pageBreaksIn(block: PMNode, blockPos: number): BreakAt[];
|
|
29
45
|
export declare function pageDecorations(): Plugin<PageMarks>;
|
|
30
46
|
/** Does nothing when the same values are already applied */
|
|
31
47
|
export declare function setPagePushes(view: EditorView, pushes: readonly BlockPush[]): void;
|