@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/dist/docx/commentOnlyChange.d.ts +3 -1
  4. package/dist/docx/commentOnlyChange.js +38 -21
  5. package/dist/docx/comments/constants.d.ts +3 -3
  6. package/dist/docx/comments/constants.js +4 -3
  7. package/dist/docx/comments/grammar.d.ts +80 -0
  8. package/dist/docx/comments/grammar.js +213 -0
  9. package/dist/docx/comments/people.js +12 -14
  10. package/dist/docx/comments/reading.d.ts +5 -3
  11. package/dist/docx/comments/reading.js +23 -20
  12. package/dist/docx/comments/verifying.d.ts +44 -0
  13. package/dist/docx/comments/verifying.js +206 -0
  14. package/dist/docx/comments/writing.js +73 -46
  15. package/dist/docx/exportDocx.js +2 -1
  16. package/dist/docx/formatting/direct.js +3 -6
  17. package/dist/docx/headersFooters.js +8 -10
  18. package/dist/docx/notes.js +3 -5
  19. package/dist/docx/paraProps.js +51 -46
  20. package/dist/docx/propsXml.d.ts +20 -2
  21. package/dist/docx/propsXml.js +72 -32
  22. package/dist/docx/relationships.js +8 -3
  23. package/dist/docx/runProps.js +34 -34
  24. package/dist/docx/sdt.js +16 -14
  25. package/dist/docx/serializeBlock.js +1 -2
  26. package/dist/docx/serializeParagraph.d.ts +2 -1
  27. package/dist/docx/serializeParagraph.js +25 -14
  28. package/dist/docx/serializeTable.js +38 -24
  29. package/dist/docx/storyProjection.d.ts +36 -0
  30. package/dist/docx/storyProjection.js +23 -0
  31. package/dist/docx/tableFormatting/editing.js +49 -73
  32. package/dist/docx/tableTemplate.js +24 -3
  33. package/dist/docx/theme.d.ts +0 -6
  34. package/dist/docx/theme.js +0 -8
  35. package/dist/editor/clipboard/images.js +17 -3
  36. package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
  37. package/dist/editor/clipboard/inlineFormatting.js +1 -0
  38. package/dist/editor/commands/breakCommands.js +4 -3
  39. package/dist/editor/commands/canRunCommand.d.ts +2 -2
  40. package/dist/editor/commands/canRunCommand.js +1 -1
  41. package/dist/editor/commands/comments/editing.js +3 -6
  42. package/dist/editor/commands/historyCommands.js +2 -1
  43. package/dist/editor/commands/lockCommands.js +10 -1
  44. package/dist/editor/commands/tabCommands.js +4 -3
  45. package/dist/editor/createEditor.js +3 -9
  46. package/dist/editor/externalClipboard.js +166 -4
  47. package/dist/editor/insertImage.js +4 -3
  48. package/dist/editor/plugins/documentProtection.d.ts +3 -3
  49. package/dist/editor/plugins/lockedContent.d.ts +4 -4
  50. package/dist/editor/plugins/lockedContent.js +1 -1
  51. package/dist/numbering/listTemplate.js +32 -10
  52. package/dist/ooxml/element.d.ts +52 -0
  53. package/dist/ooxml/element.js +49 -0
  54. package/dist/ooxml/fragment.d.ts +53 -0
  55. package/dist/ooxml/fragment.js +76 -0
  56. package/dist/ooxml/image.js +4 -3
  57. package/dist/ooxml/names.d.ts +27 -0
  58. package/dist/ooxml/names.js +29 -0
  59. package/dist/ooxml/precedence.d.ts +31 -0
  60. package/dist/ooxml/precedence.js +51 -0
  61. package/dist/ooxml/xml.d.ts +31 -0
  62. package/dist/ooxml/xml.js +27 -0
  63. package/dist/page/blockKinds.d.ts +42 -0
  64. package/dist/page/blockKinds.js +0 -0
  65. package/dist/page/measureBlocks.d.ts +14 -1
  66. package/dist/page/measureBlocks.js +31 -11
  67. package/dist/page/pageDecorations.d.ts +16 -0
  68. package/dist/page/pageDecorations.js +1 -0
  69. package/dist/page/pageLayout.d.ts +8 -43
  70. package/dist/page/pageLayout.js +28 -51
  71. package/dist/page/tableMeasurements.d.ts +10 -2
  72. package/dist/page/tableMeasurements.js +20 -16
  73. package/dist/page/usePageLayout.d.ts +15 -0
  74. package/dist/page/usePageLayout.js +30 -2
  75. package/dist/schema/attrRoles.d.ts +35 -0
  76. package/dist/schema/attrRoles.js +106 -0
  77. package/dist/schema/docxSchema.d.ts +6 -0
  78. package/dist/schema/docxSchema.js +212 -101
  79. package/dist/schema/editGuard.d.ts +99 -0
  80. package/dist/schema/editGuard.js +43 -0
  81. package/dist/schema/guards.d.ts +68 -0
  82. package/dist/schema/guards.js +82 -0
  83. package/dist/schema/locks.d.ts +8 -35
  84. package/dist/schema/locks.js +29 -64
  85. package/dist/schema/preservedGuards.d.ts +30 -0
  86. package/dist/schema/preservedGuards.js +50 -0
  87. package/dist/schema/protection.d.ts +10 -3
  88. package/dist/schema/protection.js +2 -1
  89. package/dist/schema/sourceEquality.d.ts +27 -0
  90. package/dist/schema/sourceEquality.js +65 -0
  91. package/dist/table/cellFormatting.js +2 -1
  92. package/dist/table/commands.d.ts +2 -2
  93. package/dist/table/commands.js +1 -1
  94. package/dist/table/index.d.ts +2 -2
  95. package/dist/table/merge.d.ts +2 -2
  96. package/dist/table/merge.js +1 -1
  97. package/package.json +4 -1
  98. package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
  99. package/dist/editor/plugins/bookmarkProtection.js +0 -28
  100. package/dist/editor/plugins/noteProtection.d.ts +0 -3
  101. package/dist/editor/plugins/noteProtection.js +0 -25
@@ -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
  /**
@@ -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 { isImageMime, toImageExtent, toImageSrc } from "../../ooxml/image.js";
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 extentAttribute(element) {
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[];
@@ -228,5 +228,6 @@ export {
228
228
  appendInline,
229
229
  contextFor,
230
230
  marksFor,
231
+ safeHref,
231
232
  withInlineStyle
232
233
  };
@@ -1,10 +1,11 @@
1
1
  // src/editor/commands/breakCommands.ts
2
2
  import { docxSchema } from "../../schema/index.js";
3
- import { replacementShut } from "../../schema/locks.js";
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 (editsShut(state) || replacementShut(state.selection, state.doc)) {
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 document locks and the
3
- * editor's protection without dispatching them or triggering the locked-content plugin's side
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/locks.js";
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/locks.js";
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 = node.attrs.threadImported === true && node.attrs.extensionXml === null ? nextCommentParaId(state, `comment-${id}`) : stringAttr(node.attrs.paraId) ?? nextCommentParaId(state, `comment-${id}`);
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 { historyReplay, transactionAllowed } from "../../schema/locks.js";
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: `<w:sdt><w:sdtPr><w:id w:val="${id}"/><w:lock w:val="sdtContentLocked"/></w:sdtPr>`,
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 { replacementShut } from "../../schema/locks.js";
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) || editsShut(state) || replacementShut(selection, state.doc)) {
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 inside a locked content control, whoever asked for it. It is not
75
- // optional: a document that locked a part of itself stays locked in every consumer.
76
- // The same guard refuses what the protection below shuts.
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 { Fragment, Slice } from "prosemirror-model";
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 { docxSchema } from "../schema/index.js";
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
- return element.classList.contains(editorClassNames.paragraph) ? styleIdOf(element.getAttribute("data-ppr")) : null;
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 = `<w:pPr><w:numPr><w:ilvl w:val="${ilvl}"/><w:numId w:val="${list.numId}"/></w:numPr></w: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 { replacementShut } from "../schema/locks.js";
6
- import { editsShut } from "../schema/protectionState.js";
5
+ import { editShut, selectionIntents } from "../schema/guards.js";
7
6
  function insertPosition(state) {
8
- if (editsShut(state) || replacementShut(state.selection, state.doc)) {
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 guard
3
- * and the commands read it. The guard itself stands in `lockedContent`, which enforces
4
- * `transactionAllowed` for locks and protection in one pass.
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 disallowed by the guard in `schema/locks`, which answers for the locks the
3
- * document carries and for the protection the editor runs under (`schema/protection`) alike. A
4
- * rejected IME edit also ends the browser composition on the next frame so ProseMirror cannot
5
- * remain stuck in composing state.
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/locks.js";
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 attrs = [
55
- ["w:left", indent?.startTwips],
56
- ["w:right", indent?.endTwips],
57
- ["w:hanging", indent?.hangingTwips],
58
- ["w:firstLine", indent?.firstLineTwips]
59
- ].filter((entry) => typeof entry[1] === "number").map(([name, value]) => `${name}="${value}"`).join(" ");
60
- return attrs === "" ? "" : `<w:pPr><w:ind ${attrs}/></w:pPr>`;
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 `<w:lvl w:ilvl="${ilvl}"><w:start w:val="${level.start}"/><w:numFmt w:val="${level.format}"/><w:lvlText w:val="${level.text}"/><w:lvlJc w:val="left"/>${ind}</w:lvl>`;
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 `<w:abstractNum w:abstractNumId="${abstractNumId}">${body}</w:abstractNum>`;
83
+ return elementXml(
84
+ wName("abstractNum"),
85
+ [[wName("abstractNumId"), `${abstractNumId}`]],
86
+ [body]
87
+ );
70
88
  }
71
89
  function numXml(numId, abstractNumId) {
72
- return `<w:num w:numId="${numId}"><w:abstractNumId w:val="${abstractNumId}"/></w:num>`;
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[];