@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
package/dist/docx/theme.d.ts
CHANGED
|
@@ -21,12 +21,6 @@ export interface ThemeFonts {
|
|
|
21
21
|
}
|
|
22
22
|
/** What applies to a document with no theme part, or one whose theme names no font */
|
|
23
23
|
export declare const NO_THEME_FONTS: ThemeFonts;
|
|
24
|
-
/**
|
|
25
|
-
* The theme attribute paired with each font slot.
|
|
26
|
-
* A theme reference and a name in the same slot cannot both apply, so the name wins,
|
|
27
|
-
* the way Word reads them.
|
|
28
|
-
*/
|
|
29
|
-
export declare const THEME_ATTRS: Record<string, readonly string[]>;
|
|
30
24
|
/** The font one theme reference stands for. null for a reference we cannot resolve */
|
|
31
25
|
export declare function themeFontName(fonts: ThemeFonts, value: string | null): string | null;
|
|
32
26
|
/**
|
package/dist/docx/theme.js
CHANGED
|
@@ -9,13 +9,6 @@ var NO_THEME_FONTS = {
|
|
|
9
9
|
major: NO_FONT_SCHEME,
|
|
10
10
|
minor: NO_FONT_SCHEME
|
|
11
11
|
};
|
|
12
|
-
var THEME_ATTRS = {
|
|
13
|
-
ascii: ["asciiTheme"],
|
|
14
|
-
hAnsi: ["hAnsiTheme"],
|
|
15
|
-
eastAsia: ["eastAsiaTheme"],
|
|
16
|
-
// This is the slot whose capitalization varies from document to document
|
|
17
|
-
cs: ["cstheme", "csTheme"]
|
|
18
|
-
};
|
|
19
12
|
var THEME_SLOTS = {
|
|
20
13
|
majorascii: { scheme: "major", script: "latin" },
|
|
21
14
|
majorhansi: { scheme: "major", script: "latin" },
|
|
@@ -55,7 +48,6 @@ function readThemeFonts(theme) {
|
|
|
55
48
|
}
|
|
56
49
|
export {
|
|
57
50
|
NO_THEME_FONTS,
|
|
58
|
-
THEME_ATTRS,
|
|
59
51
|
readThemeFonts,
|
|
60
52
|
themeFontName
|
|
61
53
|
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// src/editor/clipboard/images.ts
|
|
2
2
|
import { MAX_IMAGE_BYTES } from "../../docx/media.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isImageMime,
|
|
5
|
+
pxToEmu,
|
|
6
|
+
toImageExtent,
|
|
7
|
+
toImageSrc
|
|
8
|
+
} from "../../ooxml/image.js";
|
|
4
9
|
import { editorClassNames } from "../../styles/classNames.js";
|
|
5
10
|
import {
|
|
6
11
|
boundedImageSrc,
|
|
@@ -9,8 +14,7 @@ import {
|
|
|
9
14
|
readImageSource
|
|
10
15
|
} from "../imageFiles.js";
|
|
11
16
|
var PASTED_IMAGE_ATTRIBUTE = "data-docx-pasted-image";
|
|
12
|
-
function
|
|
13
|
-
if (!element.classList.contains(editorClassNames.image)) return null;
|
|
17
|
+
function declaredExtent(element) {
|
|
14
18
|
try {
|
|
15
19
|
return toImageExtent(
|
|
16
20
|
JSON.parse(element.getAttribute("data-extent") ?? "null")
|
|
@@ -19,6 +23,16 @@ function extentAttribute(element) {
|
|
|
19
23
|
return null;
|
|
20
24
|
}
|
|
21
25
|
}
|
|
26
|
+
function renderedExtent(element) {
|
|
27
|
+
const width = Number.parseInt(element.getAttribute("width") ?? "", 10);
|
|
28
|
+
const height = Number.parseInt(element.getAttribute("height") ?? "", 10);
|
|
29
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
30
|
+
return toImageExtent({ cx: pxToEmu(width), cy: pxToEmu(height) });
|
|
31
|
+
}
|
|
32
|
+
function extentAttribute(element) {
|
|
33
|
+
if (!element.classList.contains(editorClassNames.image)) return null;
|
|
34
|
+
return declaredExtent(element) ?? renderedExtent(element);
|
|
35
|
+
}
|
|
22
36
|
function fetchableUrl(source) {
|
|
23
37
|
try {
|
|
24
38
|
const url = new URL(source);
|
|
@@ -15,6 +15,13 @@ export interface InlineContext {
|
|
|
15
15
|
preserveWhitespace: boolean;
|
|
16
16
|
}
|
|
17
17
|
export type InlineElementReader = (target: PMNode[], element: HTMLElement, context: InlineContext) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The address a link may carry, whichever way it travels.
|
|
20
|
+
*
|
|
21
|
+
* A copy writes one out and a paste reads one in, and an address this rule turns down is one
|
|
22
|
+
* neither end should act on, so both ask here.
|
|
23
|
+
*/
|
|
24
|
+
export declare function safeHref(value: string | null): string | null;
|
|
18
25
|
export declare function contextFor(parent: InlineContext, element: HTMLElement, readElementStyle?: boolean): InlineContext;
|
|
19
26
|
export declare function withInlineStyle(context: InlineContext, style: InlineStyle): InlineContext;
|
|
20
27
|
export declare function marksFor(context: InlineContext): Mark[];
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// src/editor/commands/breakCommands.ts
|
|
2
2
|
import { docxSchema } from "../../schema/index.js";
|
|
3
|
-
import {
|
|
4
|
-
import { editsShut } from "../../schema/protectionState.js";
|
|
3
|
+
import { editShut, selectionIntents } from "../../schema/guards.js";
|
|
5
4
|
function insertBreak(brAttrs) {
|
|
6
5
|
return (state, dispatch) => {
|
|
7
|
-
if (
|
|
6
|
+
if (selectionIntents(state.selection, "replace").some(
|
|
7
|
+
(intent) => editShut(state, intent)
|
|
8
|
+
)) {
|
|
8
9
|
return false;
|
|
9
10
|
}
|
|
10
11
|
if (dispatch) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dry-runs external commands and checks every produced transaction against
|
|
3
|
-
*
|
|
2
|
+
* Dry-runs external commands and checks every produced transaction against every guard
|
|
3
|
+
* (`schema/guards`) without dispatching them or triggering the locked-content plugin's side
|
|
4
4
|
* effects.
|
|
5
5
|
*/
|
|
6
6
|
import type { Command, EditorState } from "prosemirror-state";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/editor/commands/canRunCommand.ts
|
|
2
|
-
import { transactionAllowed } from "../../schema/
|
|
2
|
+
import { transactionAllowed } from "../../schema/guards.js";
|
|
3
3
|
function canRunCommand(command, state) {
|
|
4
4
|
const built = [];
|
|
5
5
|
if (!command(state, (tr) => built.push(tr))) return false;
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "prosemirror-state";
|
|
5
5
|
import { commentParaId } from "../../../docx/comments.js";
|
|
6
6
|
import { docxSchema } from "../../../schema/index.js";
|
|
7
|
-
import { transactionAllowed } from "../../../schema/
|
|
7
|
+
import { transactionAllowed } from "../../../schema/guards.js";
|
|
8
8
|
import {
|
|
9
9
|
reservedCommentIds,
|
|
10
10
|
reservedCommentParaIds
|
|
@@ -178,11 +178,9 @@ function setCommentResolved(id, resolved) {
|
|
|
178
178
|
if (node.attrs.resolved === resolved) return null;
|
|
179
179
|
return {
|
|
180
180
|
...node.attrs,
|
|
181
|
-
paraId: node.attrs.threadImported === true && node.attrs.extensionXml === null ? nextCommentParaId(state, `comment-${id}`) : node.attrs.paraId,
|
|
182
181
|
resolved,
|
|
183
182
|
extensionXml: null,
|
|
184
|
-
threadImported: false
|
|
185
|
-
imported: node.attrs.commentXml !== null && node.attrs.extensionXml === null ? false : node.attrs.imported
|
|
183
|
+
threadImported: false
|
|
186
184
|
};
|
|
187
185
|
})(state, dispatch);
|
|
188
186
|
}
|
|
@@ -192,7 +190,7 @@ function addCommentReply(id, reply) {
|
|
|
192
190
|
const replyId = nextCommentId(state);
|
|
193
191
|
const date = reply.date ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
194
192
|
return updateReference(id, (node) => {
|
|
195
|
-
const parentParaId =
|
|
193
|
+
const parentParaId = stringAttr(node.attrs.paraId) ?? nextCommentParaId(state, `comment-${id}`);
|
|
196
194
|
const paraId = nextCommentParaId(state, `comment-${replyId}-${date}`, [
|
|
197
195
|
parentParaId
|
|
198
196
|
]);
|
|
@@ -202,7 +200,6 @@ function addCommentReply(id, reply) {
|
|
|
202
200
|
resolved: false,
|
|
203
201
|
extensionXml: null,
|
|
204
202
|
threadImported: false,
|
|
205
|
-
imported: node.attrs.commentXml !== null && node.attrs.extensionXml === null ? false : node.attrs.imported,
|
|
206
203
|
replies: [
|
|
207
204
|
...repliesAttr(node.attrs.replies),
|
|
208
205
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/editor/commands/historyCommands.ts
|
|
2
2
|
import { redo as historyRedo, undo as historyUndo } from "prosemirror-history";
|
|
3
|
-
import {
|
|
3
|
+
import { transactionAllowed } from "../../schema/guards.js";
|
|
4
|
+
import { historyReplay } from "../../schema/locks.js";
|
|
4
5
|
function replaying(command) {
|
|
5
6
|
return (state, dispatch, view) => {
|
|
6
7
|
const built = [];
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
} from "prosemirror-state";
|
|
5
5
|
import { namesNothing, newControlId } from "../../docx/sdt.js";
|
|
6
6
|
import { withContentLock } from "../../docx/sdtProps.js";
|
|
7
|
+
import { elementXml, openTagXml } from "../../ooxml/element.js";
|
|
8
|
+
import { wName } from "../../ooxml/names.js";
|
|
7
9
|
import { docxSchema } from "../../schema/index.js";
|
|
8
10
|
import {
|
|
9
11
|
carriesLock,
|
|
@@ -16,7 +18,14 @@ import {
|
|
|
16
18
|
import { editsShut } from "../../schema/protectionState.js";
|
|
17
19
|
function lockedControlMark(id) {
|
|
18
20
|
return docxSchema.marks.sdt.create({
|
|
19
|
-
sdtPrefix:
|
|
21
|
+
sdtPrefix: openTagXml(wName("sdt"), null) + elementXml(
|
|
22
|
+
wName("sdtPr"),
|
|
23
|
+
[],
|
|
24
|
+
[
|
|
25
|
+
elementXml(wName("id"), [[wName("val"), `${id}`]]),
|
|
26
|
+
elementXml(wName("lock"), [[wName("val"), "sdtContentLocked"]])
|
|
27
|
+
]
|
|
28
|
+
),
|
|
20
29
|
contentsLocked: true,
|
|
21
30
|
deletionLocked: true
|
|
22
31
|
});
|
|
@@ -4,8 +4,7 @@ import {
|
|
|
4
4
|
} from "prosemirror-state";
|
|
5
5
|
import { toParagraphFormat } from "../../model/format.js";
|
|
6
6
|
import { docxSchema } from "../../schema/index.js";
|
|
7
|
-
import {
|
|
8
|
-
import { editsShut } from "../../schema/protectionState.js";
|
|
7
|
+
import { editShut, selectionIntents } from "../../schema/guards.js";
|
|
9
8
|
function isOrdinaryParagraph(state) {
|
|
10
9
|
const { $from } = state.selection;
|
|
11
10
|
if ($from.parent.type !== docxSchema.nodes.paragraph || toParagraphFormat($from.parent.attrs.format)?.numbering) {
|
|
@@ -18,7 +17,9 @@ function isOrdinaryParagraph(state) {
|
|
|
18
17
|
}
|
|
19
18
|
var insertTab = (state, dispatch) => {
|
|
20
19
|
const { selection } = state;
|
|
21
|
-
if (!(selection instanceof TextSelection) || !selection.$from.sameParent(selection.$to) || !isOrdinaryParagraph(state) ||
|
|
20
|
+
if (!(selection instanceof TextSelection) || !selection.$from.sameParent(selection.$to) || !isOrdinaryParagraph(state) || selectionIntents(selection, "replace").some(
|
|
21
|
+
(intent) => editShut(state, intent)
|
|
22
|
+
)) {
|
|
22
23
|
return false;
|
|
23
24
|
}
|
|
24
25
|
if (dispatch) {
|
|
@@ -25,7 +25,6 @@ import { gridBorders, withDerivedGridBorders } from "../table/gridBorders.js";
|
|
|
25
25
|
import { documentDefaultTabStopPt, documentStyleTable } from "./documentStyles.js";
|
|
26
26
|
import { externalClipboard } from "./externalClipboard.js";
|
|
27
27
|
import { imageFiles } from "./imageFiles.js";
|
|
28
|
-
import { bookmarkProtection } from "./plugins/bookmarkProtection.js";
|
|
29
28
|
import { columnResize } from "./plugins/columnResize.js";
|
|
30
29
|
import { commentDecorations } from "./plugins/commentDecorations.js";
|
|
31
30
|
import { commentReservations } from "./plugins/commentReservations.js";
|
|
@@ -35,7 +34,6 @@ import { docxKeymap, historyKeys } from "./plugins/keymap.js";
|
|
|
35
34
|
import { linkPanel } from "./plugins/linkPanel.js";
|
|
36
35
|
import { listInputRules } from "./plugins/listInputRules.js";
|
|
37
36
|
import { lockedContent } from "./plugins/lockedContent.js";
|
|
38
|
-
import { noteProtection } from "./plugins/noteProtection.js";
|
|
39
37
|
import { numberingMarkers } from "./plugins/numberingDecorations.js";
|
|
40
38
|
import { rowResize } from "./plugins/rowResize.js";
|
|
41
39
|
import { styledParagraphs } from "./plugins/styledParagraphs.js";
|
|
@@ -71,15 +69,11 @@ function createEditorState(doc, {
|
|
|
71
69
|
// first answer for a keypress, a paste, a drop or any other DOM event, so this is the
|
|
72
70
|
// only place from which a consumer handler can win over the built-in one.
|
|
73
71
|
...consumerPlugins,
|
|
74
|
-
// Refuses every edit
|
|
75
|
-
// optional: a document that locked a part of itself stays locked in every consumer
|
|
76
|
-
//
|
|
72
|
+
// Refuses every edit no guard in `schema/guards` lets through, whoever asked for it. It is
|
|
73
|
+
// not optional: a document that locked a part of itself stays locked in every consumer, and
|
|
74
|
+
// the preserved bookmark markers and note references stay where the file put them.
|
|
77
75
|
lockedContent(),
|
|
78
76
|
documentProtection({ protection, author, editableComments }),
|
|
79
|
-
// Bookmark ranges are preserved rather than edited, so neither half may disappear.
|
|
80
|
-
bookmarkProtection(),
|
|
81
|
-
// Note bodies are display-only, so their imported main-story references stay paired.
|
|
82
|
-
noteProtection(),
|
|
83
77
|
// An orphan Comments-part entry still owns its id and must not be replaced by a new comment.
|
|
84
78
|
commentReservations(reservedCommentIds, reservedCommentParaIds),
|
|
85
79
|
history(),
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
// src/editor/externalClipboard.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DOMSerializer,
|
|
4
|
+
Fragment,
|
|
5
|
+
Slice
|
|
6
|
+
} from "prosemirror-model";
|
|
3
7
|
import { Plugin } from "prosemirror-state";
|
|
4
8
|
import { effectiveParagraphFormat, styleIdOf } from "../docx/formatting.js";
|
|
5
9
|
import { withParagraphStyle } from "../docx/paraProps.js";
|
|
6
10
|
import { MAX_ILVL, nextNumId } from "../numbering/listTemplate.js";
|
|
7
|
-
import {
|
|
11
|
+
import { elementXml } from "../ooxml/element.js";
|
|
12
|
+
import { wName } from "../ooxml/names.js";
|
|
13
|
+
import { docxSchema, isPageBreak } from "../schema/index.js";
|
|
8
14
|
import { editorClassNames } from "../styles/classNames.js";
|
|
9
15
|
import { PASTED_IMAGE_ATTRIBUTE } from "./clipboard/images.js";
|
|
10
16
|
import {
|
|
11
17
|
appendInline,
|
|
12
18
|
contextFor,
|
|
13
19
|
marksFor,
|
|
20
|
+
safeHref,
|
|
14
21
|
withInlineStyle
|
|
15
22
|
} from "./clipboard/inlineFormatting.js";
|
|
16
23
|
import { listRefOf } from "./commands/listCommands.js";
|
|
@@ -59,11 +66,149 @@ var HEADING_FALLBACKS = {
|
|
|
59
66
|
5: { bold: true, fontSizePt: 10 },
|
|
60
67
|
6: { bold: true, fontSizePt: 8 }
|
|
61
68
|
};
|
|
69
|
+
var PUBLIC_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
70
|
+
"alt",
|
|
71
|
+
"aria-label",
|
|
72
|
+
"class",
|
|
73
|
+
"colspan",
|
|
74
|
+
"height",
|
|
75
|
+
"href",
|
|
76
|
+
"lang",
|
|
77
|
+
"rowspan",
|
|
78
|
+
"src",
|
|
79
|
+
"style",
|
|
80
|
+
"width"
|
|
81
|
+
]);
|
|
82
|
+
var COPIED_STYLE_ATTRIBUTE = "data-style";
|
|
83
|
+
function isAttributes(value) {
|
|
84
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Node);
|
|
85
|
+
}
|
|
86
|
+
function publicAttributes(attrs) {
|
|
87
|
+
return Object.fromEntries(
|
|
88
|
+
Object.entries(attrs).filter(([name]) => PUBLIC_ATTRIBUTES.has(name))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
function isSpecArray(value) {
|
|
92
|
+
return Array.isArray(value) && typeof value[0] === "string";
|
|
93
|
+
}
|
|
94
|
+
function stripPrivateAttributes(spec) {
|
|
95
|
+
if (!isSpecArray(spec)) return spec;
|
|
96
|
+
const [tag, ...rest] = spec;
|
|
97
|
+
const attrs = isAttributes(rest[0]) ? publicAttributes(rest[0]) : null;
|
|
98
|
+
const children = (attrs === null ? rest : rest.slice(1)).map(
|
|
99
|
+
(child) => isSpecArray(child) ? stripPrivateAttributes(child) : child
|
|
100
|
+
);
|
|
101
|
+
return attrs === null ? [tag, ...children] : [tag, attrs, ...children];
|
|
102
|
+
}
|
|
103
|
+
function withDomAttribute(spec, name, value) {
|
|
104
|
+
if (!isSpecArray(spec)) return spec;
|
|
105
|
+
const [tag, ...rest] = spec;
|
|
106
|
+
return isAttributes(rest[0]) ? [tag, { ...rest[0], [name]: value }, ...rest.slice(1)] : [tag, { [name]: value }, ...rest];
|
|
107
|
+
}
|
|
108
|
+
function asTag(spec, tag) {
|
|
109
|
+
return isSpecArray(spec) ? [tag, ...spec.slice(1)] : spec;
|
|
110
|
+
}
|
|
111
|
+
function copiedMarkSpec(mark, spec) {
|
|
112
|
+
const stripped = stripPrivateAttributes(spec);
|
|
113
|
+
if (mark.type !== docxSchema.marks.link) return stripped;
|
|
114
|
+
const href = safeHref(
|
|
115
|
+
typeof mark.attrs.href === "string" ? mark.attrs.href : null
|
|
116
|
+
);
|
|
117
|
+
return href === null ? stripped : asTag(withDomAttribute(stripped, "href", href), "a");
|
|
118
|
+
}
|
|
119
|
+
function copiedNodeSpec(node, spec) {
|
|
120
|
+
const stripped = stripPrivateAttributes(spec);
|
|
121
|
+
if (node.type !== docxSchema.nodes.paragraph) return stripped;
|
|
122
|
+
const styleId = styleIdOf(node.attrs.pPr);
|
|
123
|
+
return styleId === null ? stripped : withDomAttribute(stripped, COPIED_STYLE_ATTRIBUTE, styleId);
|
|
124
|
+
}
|
|
125
|
+
function clipboardSerializer() {
|
|
126
|
+
const drawn = DOMSerializer.fromSchema(docxSchema);
|
|
127
|
+
const nodes = Object.fromEntries(
|
|
128
|
+
Object.entries(drawn.nodes).map(([name, toDOM]) => [
|
|
129
|
+
name,
|
|
130
|
+
(node) => copiedNodeSpec(node, toDOM(node))
|
|
131
|
+
])
|
|
132
|
+
);
|
|
133
|
+
const marks = Object.fromEntries(
|
|
134
|
+
Object.entries(drawn.marks).map(([name, toDOM]) => [
|
|
135
|
+
name,
|
|
136
|
+
(mark, inline) => copiedMarkSpec(mark, toDOM(mark, inline))
|
|
137
|
+
])
|
|
138
|
+
);
|
|
139
|
+
return new DOMSerializer(nodes, marks);
|
|
140
|
+
}
|
|
141
|
+
var UNREADABLE_BLOCKS = /* @__PURE__ */ new Set([
|
|
142
|
+
"bookmarkBlock",
|
|
143
|
+
"docxRaw",
|
|
144
|
+
"rawBlock"
|
|
145
|
+
]);
|
|
146
|
+
function stringAttr(value) {
|
|
147
|
+
return typeof value === "string" ? value : "";
|
|
148
|
+
}
|
|
149
|
+
function inlineText(node) {
|
|
150
|
+
if (node.isText) return node.text ?? "";
|
|
151
|
+
if (node.type === docxSchema.nodes.hardBreak) {
|
|
152
|
+
return isPageBreak(node.attrs.brAttrs) ? "\f" : "\n";
|
|
153
|
+
}
|
|
154
|
+
if (node.type === docxSchema.nodes.image) return stringAttr(node.attrs.alt);
|
|
155
|
+
if (node.type === docxSchema.nodes.noteReference) {
|
|
156
|
+
return node.attrs.customMarkFollows === true ? "" : stringAttr(node.attrs.label);
|
|
157
|
+
}
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
function rowsText(table) {
|
|
161
|
+
const rows = [];
|
|
162
|
+
table.forEach((row) => {
|
|
163
|
+
const cells = [];
|
|
164
|
+
row.forEach((cell) => {
|
|
165
|
+
cells.push(fragmentText(cell.content));
|
|
166
|
+
});
|
|
167
|
+
rows.push(cells.join(" "));
|
|
168
|
+
});
|
|
169
|
+
return rows.join("\n");
|
|
170
|
+
}
|
|
171
|
+
function blockText(node) {
|
|
172
|
+
if (node.type === docxSchema.nodes.table) return rowsText(node);
|
|
173
|
+
if (UNREADABLE_BLOCKS.has(node.type.name)) return "";
|
|
174
|
+
return fragmentText(node.content);
|
|
175
|
+
}
|
|
176
|
+
function fragmentText(fragment) {
|
|
177
|
+
const pieces = [];
|
|
178
|
+
fragment.forEach((child) => {
|
|
179
|
+
pieces.push(child.isInline ? inlineText(child) : blockText(child));
|
|
180
|
+
});
|
|
181
|
+
return pieces.join(fragment.firstChild?.isInline === true ? "" : "\n");
|
|
182
|
+
}
|
|
183
|
+
function bareWrappers(content, openStart, openEnd) {
|
|
184
|
+
const wrapper = content.firstChild;
|
|
185
|
+
if (openStart <= 1 || openEnd <= 1 || content.childCount !== 1)
|
|
186
|
+
return content;
|
|
187
|
+
if (wrapper === null || wrapper.childCount !== 1) return content;
|
|
188
|
+
return Fragment.from(
|
|
189
|
+
wrapper.type.create(
|
|
190
|
+
null,
|
|
191
|
+
bareWrappers(wrapper.content, openStart - 1, openEnd - 1),
|
|
192
|
+
wrapper.marks
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
function copiedSlice(slice) {
|
|
197
|
+
return new Slice(
|
|
198
|
+
bareWrappers(slice.content, slice.openStart, slice.openEnd),
|
|
199
|
+
slice.openStart,
|
|
200
|
+
slice.openEnd
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
function clipboardText(slice) {
|
|
204
|
+
return fragmentText(slice.content);
|
|
205
|
+
}
|
|
62
206
|
function normalizedStyleName(value) {
|
|
63
207
|
return value.toLowerCase().replace(/[\s_-]+/g, "");
|
|
64
208
|
}
|
|
65
209
|
function copiedStyleId(element) {
|
|
66
|
-
|
|
210
|
+
if (!element.classList.contains(editorClassNames.paragraph)) return null;
|
|
211
|
+
return element.getAttribute(COPIED_STYLE_ATTRIBUTE) ?? styleIdOf(element.getAttribute("data-ppr"));
|
|
67
212
|
}
|
|
68
213
|
function headingLevel(element, sourceStyleId) {
|
|
69
214
|
const direct = HEADING_LEVELS[element.tagName];
|
|
@@ -116,7 +261,20 @@ function blockContext(state, parent, element, preserveParagraphStyle) {
|
|
|
116
261
|
function listParagraphAttrs(state, list) {
|
|
117
262
|
if (!list || list.numId === null) return null;
|
|
118
263
|
const ilvl = Math.min(MAX_ILVL, list.level);
|
|
119
|
-
const pPr =
|
|
264
|
+
const pPr = elementXml(
|
|
265
|
+
wName("pPr"),
|
|
266
|
+
[],
|
|
267
|
+
[
|
|
268
|
+
elementXml(
|
|
269
|
+
wName("numPr"),
|
|
270
|
+
[],
|
|
271
|
+
[
|
|
272
|
+
elementXml(wName("ilvl"), [[wName("val"), `${ilvl}`]]),
|
|
273
|
+
elementXml(wName("numId"), [[wName("val"), `${list.numId}`]])
|
|
274
|
+
]
|
|
275
|
+
)
|
|
276
|
+
]
|
|
277
|
+
);
|
|
120
278
|
return {
|
|
121
279
|
pPr,
|
|
122
280
|
format: effectiveParagraphFormat(pPr, documentParagraphFormatting(state))
|
|
@@ -294,8 +452,12 @@ function insertClipboardData(view, data) {
|
|
|
294
452
|
insertPlainText(view, data.text ?? "");
|
|
295
453
|
}
|
|
296
454
|
function externalClipboard() {
|
|
455
|
+
const serializer = clipboardSerializer();
|
|
297
456
|
return new Plugin({
|
|
298
457
|
props: {
|
|
458
|
+
clipboardSerializer: serializer,
|
|
459
|
+
clipboardTextSerializer: clipboardText,
|
|
460
|
+
transformCopied: copiedSlice,
|
|
299
461
|
handlePaste(view, event) {
|
|
300
462
|
insertClipboardData(view, {
|
|
301
463
|
html: event.clipboardData?.getData("text/html"),
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import { insertPoint } from "prosemirror-transform";
|
|
3
3
|
import { toImageExtent, toImageSrc } from "../ooxml/image.js";
|
|
4
4
|
import { docxSchema } from "../schema/index.js";
|
|
5
|
-
import {
|
|
6
|
-
import { editsShut } from "../schema/protectionState.js";
|
|
5
|
+
import { editShut, selectionIntents } from "../schema/guards.js";
|
|
7
6
|
function insertPosition(state) {
|
|
8
|
-
if (
|
|
7
|
+
if (selectionIntents(state.selection, "replace").some(
|
|
8
|
+
(intent) => editShut(state, intent)
|
|
9
|
+
)) {
|
|
9
10
|
return null;
|
|
10
11
|
}
|
|
11
12
|
return insertPoint(state.doc, state.selection.from, docxSchema.nodes.image);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Holds the protection the editor runs under (`schema/protection`) in the state, where the
|
|
3
|
-
* and the commands read it. The
|
|
4
|
-
* `
|
|
2
|
+
* Holds the protection the editor runs under (`schema/protection`) in the state, where the guards
|
|
3
|
+
* and the commands read it. The runtime filter stands in `lockedContent`, which enforces every
|
|
4
|
+
* guard `schema/guards` registers, the protection among them, in one pass.
|
|
5
5
|
*/
|
|
6
6
|
import { Plugin, type Transaction } from "prosemirror-state";
|
|
7
7
|
import type { EditableComments, EditingProtection, ProtectionState } from "../../schema/protection";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rejects transactions
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Rejects transactions no guard `schema/guards` registers would let through, the locks the document
|
|
3
|
+
* carries and the protection the editor runs under (`schema/protection`) among them. A rejected IME
|
|
4
|
+
* edit also ends the browser composition on the next frame so ProseMirror cannot remain stuck in
|
|
5
|
+
* composing state.
|
|
6
6
|
*/
|
|
7
7
|
import { Plugin } from "prosemirror-state";
|
|
8
8
|
export declare function lockedContent(): Plugin;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/editor/plugins/lockedContent.ts
|
|
2
2
|
import { Plugin } from "prosemirror-state";
|
|
3
|
-
import { transactionAllowed } from "../../schema/
|
|
3
|
+
import { transactionAllowed } from "../../schema/guards.js";
|
|
4
4
|
function endComposition(view) {
|
|
5
5
|
if (!view.composing) return;
|
|
6
6
|
const marks = view.state.storedMarks ?? view.state.selection.$from.marks();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// src/numbering/listTemplate.ts
|
|
2
|
+
import { elementXml } from "../ooxml/element.js";
|
|
3
|
+
import { wName } from "../ooxml/names.js";
|
|
2
4
|
var LEVEL_COUNT = 9;
|
|
3
5
|
var MAX_ILVL = LEVEL_COUNT - 1;
|
|
4
6
|
var NUMBER_CYCLE = [
|
|
@@ -51,25 +53,45 @@ function nextNumId(used, kind) {
|
|
|
51
53
|
return listKindOf(from) === kind ? from : from + 1;
|
|
52
54
|
}
|
|
53
55
|
function indXml(indent) {
|
|
54
|
-
const
|
|
55
|
-
["
|
|
56
|
-
["
|
|
57
|
-
["
|
|
58
|
-
["
|
|
59
|
-
]
|
|
60
|
-
|
|
56
|
+
const slots = [
|
|
57
|
+
["left", indent?.startTwips ?? null],
|
|
58
|
+
["right", indent?.endTwips ?? null],
|
|
59
|
+
["hanging", indent?.hangingTwips ?? null],
|
|
60
|
+
["firstLine", indent?.firstLineTwips ?? null]
|
|
61
|
+
];
|
|
62
|
+
const attrs = slots.filter((slot) => slot[1] !== null).map(([name, twips]) => [wName(name), `${twips}`]);
|
|
63
|
+
if (attrs.length === 0) return "";
|
|
64
|
+
return elementXml(wName("pPr"), [], [elementXml(wName("ind"), attrs)]);
|
|
61
65
|
}
|
|
62
66
|
function levelXml(ilvl, level) {
|
|
63
67
|
const ind = indXml(level.indent);
|
|
64
|
-
return
|
|
68
|
+
return elementXml(
|
|
69
|
+
wName("lvl"),
|
|
70
|
+
[[wName("ilvl"), `${ilvl}`]],
|
|
71
|
+
[
|
|
72
|
+
elementXml(wName("start"), [[wName("val"), `${level.start}`]]),
|
|
73
|
+
elementXml(wName("numFmt"), [[wName("val"), level.format]]),
|
|
74
|
+
elementXml(wName("lvlText"), [[wName("val"), level.text]]),
|
|
75
|
+
elementXml(wName("lvlJc"), [[wName("val"), "left"]]),
|
|
76
|
+
ind
|
|
77
|
+
]
|
|
78
|
+
);
|
|
65
79
|
}
|
|
66
80
|
function abstractNumXml(abstractNumId, kind) {
|
|
67
81
|
const levels = TEMPLATES[kind].levels;
|
|
68
82
|
const body = Array.from(levels.entries()).map(([ilvl, level]) => levelXml(ilvl, level)).join("");
|
|
69
|
-
return
|
|
83
|
+
return elementXml(
|
|
84
|
+
wName("abstractNum"),
|
|
85
|
+
[[wName("abstractNumId"), `${abstractNumId}`]],
|
|
86
|
+
[body]
|
|
87
|
+
);
|
|
70
88
|
}
|
|
71
89
|
function numXml(numId, abstractNumId) {
|
|
72
|
-
return
|
|
90
|
+
return elementXml(
|
|
91
|
+
wName("num"),
|
|
92
|
+
[[wName("numId"), `${numId}`]],
|
|
93
|
+
[elementXml(wName("abstractNumId"), [[wName("val"), `${abstractNumId}`]])]
|
|
94
|
+
);
|
|
73
95
|
}
|
|
74
96
|
export {
|
|
75
97
|
LEVEL_COUNT,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing one element out as text, and merging the attributes of one.
|
|
3
|
+
*
|
|
4
|
+
* Everything here works on strings, because that is what the writers hold: a fragment the file
|
|
5
|
+
* arrived with is carried around as its original text, and an edit swaps out one element or one
|
|
6
|
+
* attribute of it. Rebuilding a parsed element instead would rewrite markup nobody touched.
|
|
7
|
+
*
|
|
8
|
+
* An attribute is matched by its local part rather than by its written name, so a document that
|
|
9
|
+
* bound the WordprocessingML namespace to a prefix of its own is edited in the spelling it chose.
|
|
10
|
+
*/
|
|
11
|
+
import { type KnownPrefix } from "./names";
|
|
12
|
+
/** One attribute as it is written: the whole name, prefix included, and the value before escaping */
|
|
13
|
+
export type XmlAttr = readonly [name: string, value: string];
|
|
14
|
+
/** `a="b" c="d"`. Empty for no attributes, so an opening tag needs no separator of its own */
|
|
15
|
+
export declare function attrsText(attrs: readonly XmlAttr[]): string;
|
|
16
|
+
/**
|
|
17
|
+
* `<name a="b"/>`, or `<name a="b">children</name>` for an element that holds something.
|
|
18
|
+
*
|
|
19
|
+
* Attribute values are escaped here; children are spliced in as they are, being markup already, so
|
|
20
|
+
* a caller writing text as a child escapes it itself. An element handed no child at all closes on
|
|
21
|
+
* its own, which means a caller that wants an empty pair of tags passes the empty text as a child.
|
|
22
|
+
*/
|
|
23
|
+
export declare function elementXml(name: string, attrs: readonly XmlAttr[], children?: readonly string[]): string;
|
|
24
|
+
/**
|
|
25
|
+
* The namespace declaration a fragment carries when the part it is spliced into does not declare
|
|
26
|
+
* the prefix itself.
|
|
27
|
+
*/
|
|
28
|
+
export declare function xmlnsAttr(prefix: KnownPrefix): XmlAttr;
|
|
29
|
+
/**
|
|
30
|
+
* The attributes with the one of this local name written as `value`, or removed when it is null.
|
|
31
|
+
*
|
|
32
|
+
* One already there keeps the slot it sat in and the name it was written under, so an edited
|
|
33
|
+
* element reads as the original did. One that is not there yet goes on the end under the `w`
|
|
34
|
+
* prefix.
|
|
35
|
+
*/
|
|
36
|
+
export declare function withAttr(attrs: readonly XmlAttr[], name: string, value: string | null): XmlAttr[];
|
|
37
|
+
export declare function withoutAttrs(attrs: readonly XmlAttr[], localNames: readonly string[]): XmlAttr[];
|
|
38
|
+
export declare function attrValue(attrs: readonly XmlAttr[], localName: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* An opening tag whose attributes are the ones the original wrote.
|
|
41
|
+
*
|
|
42
|
+
* The text is spliced in exactly as it arrived rather than parsed and written again, so the
|
|
43
|
+
* spelling, the order, and the escaping a producer chose all survive being edited.
|
|
44
|
+
*/
|
|
45
|
+
export declare function openTagXml(name: string, rawAttrs: string | null): string;
|
|
46
|
+
/** The same for an element that closes on its own */
|
|
47
|
+
export declare function emptyTagXml(name: string, rawAttrs: string | null): string;
|
|
48
|
+
/**
|
|
49
|
+
* The attributes of a parsed element as pairs, for a caller that still holds the element rather
|
|
50
|
+
* than its text.
|
|
51
|
+
*/
|
|
52
|
+
export declare function attrPairs(el: Element | null): XmlAttr[];
|