@portone/docx-editor 0.5.0 → 0.5.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 (42) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/CONTRIBUTING.md +2 -0
  3. package/dist/docx/formatting/styles.d.ts +0 -2
  4. package/dist/docx/formatting/styles.js +2 -15
  5. package/dist/docx/identities.js +17 -26
  6. package/dist/docx/importParagraph.d.ts +6 -5
  7. package/dist/docx/importParagraph.js +15 -62
  8. package/dist/docx/importPolicy.js +10 -5
  9. package/dist/docx/serializeParagraph.d.ts +5 -4
  10. package/dist/docx/serializeParagraph.js +22 -58
  11. package/dist/docx/tableTemplate.d.ts +25 -0
  12. package/dist/docx/tableTemplate.js +30 -16
  13. package/dist/docx/wrappers.d.ts +67 -0
  14. package/dist/docx/wrappers.js +117 -0
  15. package/dist/editor/clipboard/blockReaders.d.ts +74 -0
  16. package/dist/editor/clipboard/blockReaders.js +193 -0
  17. package/dist/editor/clipboard/htmlReader.d.ts +2 -6
  18. package/dist/editor/clipboard/htmlReader.js +98 -24
  19. package/dist/editor/clipboard/inlineFormatting.d.ts +0 -7
  20. package/dist/editor/clipboard/inlineFormatting.js +1 -7
  21. package/dist/editor/clipboard/plugin.js +7 -139
  22. package/dist/editor/clipboard/readContext.d.ts +10 -0
  23. package/dist/editor/clipboard/readContext.js +8 -2
  24. package/dist/editor/clipboard/source.d.ts +12 -0
  25. package/dist/editor/clipboard/source.js +21 -0
  26. package/dist/editor/commands/comments/editing.js +2 -1
  27. package/dist/editor/commands/linkCommands.js +20 -5
  28. package/dist/editor/commands/lockCommands.js +59 -16
  29. package/dist/editor/commands/paragraphCommands.js +2 -2
  30. package/dist/ooxml/props.d.ts +2 -0
  31. package/dist/ooxml/props.js +14 -0
  32. package/dist/schema/attrRoles.js +8 -3
  33. package/dist/schema/clipboard.d.ts +72 -0
  34. package/dist/schema/clipboard.js +288 -0
  35. package/dist/schema/docxSchema.js +27 -27
  36. package/dist/schema/locks.d.ts +3 -0
  37. package/dist/schema/locks.js +29 -12
  38. package/dist/schema/rendering.d.ts +14 -0
  39. package/dist/schema/rendering.js +24 -11
  40. package/dist/schema/wrappers.d.ts +58 -0
  41. package/dist/schema/wrappers.js +77 -0
  42. package/package.json +5 -4
@@ -1,8 +1,7 @@
1
1
  // src/editor/clipboard/htmlReader.ts
2
2
  import { Fragment, Slice } from "prosemirror-model";
3
3
  import {
4
- paragraphAttrsFor,
5
- styleIdOf
4
+ paragraphAttrsFor
6
5
  } from "../../docx/formatting.js";
7
6
  import {
8
7
  withListNumbering,
@@ -18,9 +17,14 @@ import {
18
17
  MAX_ILVL,
19
18
  templateList
20
19
  } from "../../numbering/listTemplate.js";
20
+ import { styleIdOf } from "../../ooxml/props.js";
21
21
  import { docxSchema } from "../../schema/index.js";
22
+ import { COPIED_STYLE_ATTRIBUTE } from "../../schema/clipboard.js";
22
23
  import { editorClassNames } from "../../styles/classNames.js";
23
24
  import { numIdsIn } from "../commands/listCommands.js";
25
+ import {
26
+ DEFAULT_BLOCK_READERS
27
+ } from "./blockReaders.js";
24
28
  import { PASTED_IMAGE_ATTRIBUTE } from "./images.js";
25
29
  import {
26
30
  appendInline,
@@ -28,6 +32,7 @@ import {
28
32
  marksFor,
29
33
  withInlineStyle
30
34
  } from "./inlineFormatting.js";
35
+ import { detectHtmlSource } from "./source.js";
31
36
  var BLOCK_TAGS = /* @__PURE__ */ new Set([
32
37
  "ADDRESS",
33
38
  "ARTICLE",
@@ -45,6 +50,7 @@ var BLOCK_TAGS = /* @__PURE__ */ new Set([
45
50
  "PRE",
46
51
  "SECTION"
47
52
  ]);
53
+ var BLOCK_SELECTOR = [...BLOCK_TAGS, "OL", "UL", "TABLE"].map((tag) => tag.toLowerCase()).join(",");
48
54
  var HEADING_LEVELS = {
49
55
  H1: 1,
50
56
  H2: 2,
@@ -61,7 +67,6 @@ var HEADING_FALLBACKS = {
61
67
  5: { bold: true, fontSizePt: 10 },
62
68
  6: { bold: true, fontSizePt: 8 }
63
69
  };
64
- var COPIED_STYLE_ATTRIBUTE = "data-style";
65
70
  function normalizedStyleName(value) {
66
71
  return value.toLowerCase().replace(/[\s_-]+/g, "");
67
72
  }
@@ -127,18 +132,24 @@ function paragraphAttrs(formatting, list, paragraph) {
127
132
  return props ? { pPr: props.pPr, ...paragraphAttrsFor(props.pPr, formatting) } : null;
128
133
  }
129
134
  var HtmlReader = class {
130
- constructor(context, preserveParagraphStyles) {
135
+ constructor(context, preserveParagraphStyles, blockReaders) {
131
136
  this.context = context;
132
137
  this.preserveParagraphStyles = preserveParagraphStyles;
138
+ this.blockReaders = blockReaders;
133
139
  this.formatting = context.formatting;
134
140
  this.used = new Set(context.numbering.used);
135
141
  this.canCreateLists = context.numbering.canCreate;
136
142
  }
137
143
  context;
138
144
  preserveParagraphStyles;
145
+ blockReaders;
139
146
  blocks = [];
140
147
  used;
141
148
  canCreateLists;
149
+ /** How deep the reading stands inside the tables it is reading */
150
+ tableDepth = 0;
151
+ /** The number each list a block reader named has taken, so its items join one list */
152
+ listNumbers = /* @__PURE__ */ new Map();
142
153
  /**
143
154
  * What the pasted paragraphs are resolved against: the document's context, holding the
144
155
  * definition of each pasted list as it is read, so that an item is drawn against the list it
@@ -155,6 +166,9 @@ var HtmlReader = class {
155
166
  }
156
167
  appendInline(target, node, context) {
157
168
  appendInline(target, node, context, (content, element, inline) => {
169
+ if (this.context.source === "word" && element.tagName === "O:P") {
170
+ return true;
171
+ }
158
172
  if (element.tagName !== "IMG") return false;
159
173
  const token = element.getAttribute(PASTED_IMAGE_ATTRIBUTE);
160
174
  const image = token === null ? void 0 : this.context.images.get(token);
@@ -173,17 +187,65 @@ var HtmlReader = class {
173
187
  return true;
174
188
  });
175
189
  }
190
+ /**
191
+ * One paragraph of the reading. An item of a list this document may not start keeps its marker
192
+ * as text, since the list it names is one the document will not be numbering.
193
+ */
194
+ paragraphNode(content, list = null, paragraph = null) {
195
+ const marker = list && list.numId === null ? [docxSchema.text(list.kind === "bullet" ? "\u2022 " : "1. ")] : [];
196
+ return docxSchema.nodes.paragraph.create(
197
+ paragraphAttrs(this.formatting, list, paragraph),
198
+ [...marker, ...content]
199
+ );
200
+ }
176
201
  addParagraph(content, list = null, paragraph = null) {
177
- if (list && list.numId === null) {
178
- const marker = list.kind === "bullet" ? "\u2022 " : "1. ";
179
- content.unshift(docxSchema.text(marker));
202
+ this.blocks.push(this.paragraphNode(content, list, paragraph));
203
+ }
204
+ /** The blocks a nested reading produces, kept apart from the ones read so far */
205
+ collect(read) {
206
+ const outer = this.blocks;
207
+ this.blocks = [];
208
+ read();
209
+ const inner = this.blocks;
210
+ this.blocks = outer;
211
+ return inner;
212
+ }
213
+ /** What the first reader to answer for this element reads it as, or null where none does */
214
+ readBlock(element, context, host) {
215
+ for (const reader of this.blockReaders) {
216
+ const read = reader.read(element, context, host);
217
+ if (read !== null) return read;
180
218
  }
181
- this.blocks.push(
182
- docxSchema.nodes.paragraph.create(
183
- paragraphAttrs(this.formatting, list, paragraph),
184
- content
185
- )
186
- );
219
+ return null;
220
+ }
221
+ /**
222
+ * What a block reader may ask of this reading. One is made per level of the reading, since a
223
+ * reader asks whether it stands inside a table and that answer is the level's own.
224
+ */
225
+ hostView() {
226
+ return {
227
+ context: this.context,
228
+ inTable: this.tableDepth > 0,
229
+ readCell: (cell, inline) => {
230
+ this.tableDepth += 1;
231
+ const blocks = this.collect(() => this.readFlow(cell, inline));
232
+ this.tableDepth -= 1;
233
+ return blocks;
234
+ },
235
+ readInline: (nodes, inline) => {
236
+ const target = [];
237
+ for (const node of nodes) this.appendInline(target, node, inline);
238
+ return target;
239
+ },
240
+ paragraph: (content, list) => this.paragraphNode(content, list ?? null),
241
+ listNumber: (key, kind) => {
242
+ const known = this.listNumbers.get(key);
243
+ if (known !== void 0) return known;
244
+ const numId = this.takeNumId(kind);
245
+ this.listNumbers.set(key, numId);
246
+ return numId;
247
+ }
248
+ };
187
249
  }
188
250
  readFlow(parent, context, paragraph = null) {
189
251
  let inline = [];
@@ -192,12 +254,17 @@ var HtmlReader = class {
192
254
  this.addParagraph(inline, null, paragraph);
193
255
  inline = [];
194
256
  };
257
+ const host = this.hostView();
195
258
  for (const child of parent.childNodes) {
196
259
  const element = child.nodeType === child.ELEMENT_NODE ? child : null;
197
- if (element?.tagName === "UL" || element?.tagName === "OL") {
260
+ const read = element && this.readBlock(element, context, host);
261
+ if (read) {
262
+ flush();
263
+ this.blocks.push(...read);
264
+ } else if (element?.tagName === "UL" || element?.tagName === "OL") {
198
265
  flush();
199
266
  this.readList(element, context, 0, null);
200
- } else if (element && BLOCK_TAGS.has(element.tagName)) {
267
+ } else if (element && (BLOCK_TAGS.has(element.tagName) || element.querySelector(BLOCK_SELECTOR) !== null)) {
201
268
  flush();
202
269
  const before = this.blocks.length;
203
270
  const block = blockContext(
@@ -238,18 +305,23 @@ var HtmlReader = class {
238
305
  numId: inherited?.kind === kind ? inherited.numId : this.takeNumId(kind),
239
306
  level
240
307
  };
241
- const items = Array.from(list.children).filter(
242
- (child) => child.tagName === "LI"
308
+ const items = [...list.children].flatMap(
309
+ (child) => child instanceof HTMLElement && child.tagName === "LI" ? [child] : []
243
310
  );
311
+ const host = this.hostView();
244
312
  for (const item of items) {
245
313
  const itemContext = contextFor(context, item);
246
314
  const content = [];
315
+ const after = [];
247
316
  for (const child of item.childNodes) {
248
- const nested = child.nodeType === child.ELEMENT_NODE ? child : null;
317
+ const nested = child instanceof HTMLElement ? child : null;
249
318
  if (nested?.tagName === "UL" || nested?.tagName === "OL") continue;
250
- this.appendInline(content, child, itemContext);
319
+ const read = nested && this.readBlock(nested, itemContext, host);
320
+ if (read) after.push(...read);
321
+ else this.appendInline(content, child, itemContext);
251
322
  }
252
323
  this.addParagraph(content, listContext);
324
+ this.blocks.push(...after);
253
325
  for (const nested of Array.from(item.children)) {
254
326
  if (nested.tagName === "UL" || nested.tagName === "OL") {
255
327
  this.readList(
@@ -270,12 +342,15 @@ function sliceDepth(root) {
270
342
  10
271
343
  );
272
344
  if (Number.isFinite(openStart)) return openStart === 0 ? 0 : 1;
273
- const blocks = [...BLOCK_TAGS, "OL", "UL"].map((tag) => tag.toLowerCase());
274
- return root.querySelector(blocks.join(",")) ? 0 : 1;
345
+ return root.querySelector(BLOCK_SELECTOR) ? 0 : 1;
275
346
  }
276
- function readHtml(root, context) {
347
+ function readHtml(root, context, blockReaders = DEFAULT_BLOCK_READERS) {
277
348
  const open = sliceDepth(root);
278
- const reader = new HtmlReader(context, open === 0);
349
+ const reader = new HtmlReader(
350
+ { ...context, source: detectHtmlSource(root) },
351
+ open === 0,
352
+ blockReaders
353
+ );
279
354
  const blocks = reader.read(root);
280
355
  return blocks.length === 0 ? null : {
281
356
  slice: new Slice(Fragment.fromArray([...blocks]), open, open),
@@ -295,7 +370,6 @@ function withPastedContent(tr, content) {
295
370
  );
296
371
  }
297
372
  export {
298
- COPIED_STYLE_ATTRIBUTE,
299
373
  readHtml,
300
374
  readHtmlSlice,
301
375
  withPastedContent
@@ -15,13 +15,6 @@ 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;
25
18
  export declare function contextFor(parent: InlineContext, element: HTMLElement, readElementStyle?: boolean): InlineContext;
26
19
  export declare function withInlineStyle(context: InlineContext, style: InlineStyle): InlineContext;
27
20
  export declare function marksFor(context: InlineContext): Mark[];
@@ -3,6 +3,7 @@ import { rPrOf } from "../../docx/formatting.js";
3
3
  import { readRunProps } from "../../docx/runProps.js";
4
4
  import { HALF_POINTS_PER_PT, ST_HpsMeasure } from "../../ooxml/simpleTypes.js";
5
5
  import { docxSchema } from "../../schema/index.js";
6
+ import { safeHref } from "../../schema/clipboard.js";
6
7
  import { editorClassNames } from "../../styles/classNames.js";
7
8
  var IGNORED_TAGS = /* @__PURE__ */ new Set([
8
9
  "HEAD",
@@ -135,12 +136,6 @@ function styleOf(parent, element) {
135
136
  if (background !== null) style.background = background;
136
137
  return style;
137
138
  }
138
- function safeHref(value) {
139
- const href = value?.trim() ?? "";
140
- if (href === "") return null;
141
- if (/^(?:https?|mailto|tel):/i.test(href)) return href;
142
- return /^(?:[./]|#)/.test(href) ? href : null;
143
- }
144
139
  function contextFor(parent, element, readElementStyle = true) {
145
140
  const copiedEditorLink = element.classList.contains(editorClassNames.link) ? element.getAttribute("data-href") : null;
146
141
  return {
@@ -217,6 +212,5 @@ export {
217
212
  appendInline,
218
213
  contextFor,
219
214
  marksFor,
220
- safeHref,
221
215
  withInlineStyle
222
216
  };
@@ -1,25 +1,18 @@
1
1
  // src/editor/clipboard/plugin.ts
2
- import {
3
- DOMSerializer,
4
- Fragment,
5
- Slice
6
- } from "prosemirror-model";
2
+ import { Fragment, Slice } from "prosemirror-model";
7
3
  import { Plugin } from "prosemirror-state";
8
- import { styleIdOf } from "../../docx/formatting.js";
9
4
  import {
10
5
  NEW_LISTS_ATTR,
11
6
  NO_NEW_LISTS,
12
7
  newListsOf,
13
8
  newListsValue
14
9
  } from "../../numbering/listRegistry.js";
15
- import { docxSchema, isPageBreak } from "../../schema/index.js";
10
+ import { clipboardSerializer, clipboardText } from "../../schema/clipboard.js";
16
11
  import { numIdsIn } from "../commands/listCommands.js";
17
12
  import { documentOf } from "../editorDocument.js";
18
13
  import { insertPlainText } from "../plainText.js";
19
14
  import { moveCaretToDrop } from "../plugins/dropCaret.js";
20
15
  import { documentNumbering } from "../plugins/numberingDecorations.js";
21
- import { COPIED_STYLE_ATTRIBUTE } from "./htmlReader.js";
22
- import { safeHref } from "./inlineFormatting.js";
23
16
  import {
24
17
  INTERNAL_TOKEN_ATTRIBUTE,
25
18
  internalTokenOf,
@@ -35,132 +28,6 @@ import {
35
28
  DEFAULT_READERS,
36
29
  plainTextSlice
37
30
  } from "./readers.js";
38
- var PUBLIC_ATTRIBUTES = /* @__PURE__ */ new Set([
39
- "alt",
40
- "aria-label",
41
- "class",
42
- "colspan",
43
- "height",
44
- "href",
45
- "lang",
46
- "rowspan",
47
- "src",
48
- "style",
49
- "width"
50
- ]);
51
- function isAttributes(value) {
52
- return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Node);
53
- }
54
- function publicAttributes(attrs) {
55
- return Object.fromEntries(
56
- Object.entries(attrs).filter(([name]) => PUBLIC_ATTRIBUTES.has(name))
57
- );
58
- }
59
- function isSpecArray(value) {
60
- return Array.isArray(value) && typeof value[0] === "string";
61
- }
62
- function stripPrivateAttributes(spec) {
63
- if (!isSpecArray(spec)) return spec;
64
- const [tag, ...rest] = spec;
65
- const attrs = isAttributes(rest[0]) ? publicAttributes(rest[0]) : null;
66
- const children = (attrs === null ? rest : rest.slice(1)).map(
67
- (child) => isSpecArray(child) ? stripPrivateAttributes(child) : child
68
- );
69
- return attrs === null ? [tag, ...children] : [tag, attrs, ...children];
70
- }
71
- function withDomAttribute(spec, name, value) {
72
- if (!isSpecArray(spec)) return spec;
73
- const [tag, ...rest] = spec;
74
- return isAttributes(rest[0]) ? [tag, { ...rest[0], [name]: value }, ...rest.slice(1)] : [tag, { [name]: value }, ...rest];
75
- }
76
- function asTag(spec, tag) {
77
- return isSpecArray(spec) ? [tag, ...spec.slice(1)] : spec;
78
- }
79
- function copiedMarkSpec(mark, spec) {
80
- const stripped = stripPrivateAttributes(spec);
81
- if (mark.type !== docxSchema.marks.link) return stripped;
82
- const href = safeHref(
83
- typeof mark.attrs.href === "string" ? mark.attrs.href : null
84
- );
85
- return href === null ? stripped : asTag(withDomAttribute(stripped, "href", href), "a");
86
- }
87
- function copiedNodeSpec(node, spec) {
88
- const stripped = stripPrivateAttributes(spec);
89
- if (node.type !== docxSchema.nodes.paragraph) return stripped;
90
- const styleId = styleIdOf(node.attrs.pPr);
91
- return styleId === null ? stripped : withDomAttribute(stripped, COPIED_STYLE_ATTRIBUTE, styleId);
92
- }
93
- function copiedNodes() {
94
- const drawn = DOMSerializer.fromSchema(docxSchema);
95
- return Object.fromEntries(
96
- Object.entries(drawn.nodes).map(([name, toDOM]) => [
97
- name,
98
- (node) => copiedNodeSpec(node, toDOM(node))
99
- ])
100
- );
101
- }
102
- function copiedMarks() {
103
- const drawn = DOMSerializer.fromSchema(docxSchema);
104
- return Object.fromEntries(
105
- Object.entries(drawn.marks).map(([name, toDOM]) => [
106
- name,
107
- (mark, inline) => copiedMarkSpec(mark, toDOM(mark, inline))
108
- ])
109
- );
110
- }
111
- var ClipboardSerializer = class extends DOMSerializer {
112
- constructor(tokenOf) {
113
- super(copiedNodes(), copiedMarks());
114
- this.tokenOf = tokenOf;
115
- }
116
- tokenOf;
117
- serializeFragment(fragment, options, target) {
118
- const dom = super.serializeFragment(fragment, options, target);
119
- const token = target === void 0 ? this.tokenOf() : null;
120
- if (token !== null) {
121
- dom.firstElementChild?.setAttribute(INTERNAL_TOKEN_ATTRIBUTE, token);
122
- }
123
- return dom;
124
- }
125
- };
126
- var UNREADABLE_BLOCKS = /* @__PURE__ */ new Set(["rawBlock"]);
127
- function stringAttr(value) {
128
- return typeof value === "string" ? value : "";
129
- }
130
- function inlineText(node) {
131
- if (node.isText) return node.text ?? "";
132
- if (node.type === docxSchema.nodes.hardBreak) {
133
- return isPageBreak(node.attrs.brAttrs) ? "\f" : "\n";
134
- }
135
- if (node.type === docxSchema.nodes.image) return stringAttr(node.attrs.alt);
136
- if (node.type === docxSchema.nodes.noteReference) {
137
- return node.attrs.customMarkFollows === true ? "" : stringAttr(node.attrs.label);
138
- }
139
- return "";
140
- }
141
- function rowsText(table) {
142
- const rows = [];
143
- table.forEach((row) => {
144
- const cells = [];
145
- row.forEach((cell) => {
146
- cells.push(fragmentText(cell.content));
147
- });
148
- rows.push(cells.join(" "));
149
- });
150
- return rows.join("\n");
151
- }
152
- function blockText(node) {
153
- if (node.type === docxSchema.nodes.table) return rowsText(node);
154
- if (UNREADABLE_BLOCKS.has(node.type.name)) return "";
155
- return fragmentText(node.content);
156
- }
157
- function fragmentText(fragment) {
158
- const pieces = [];
159
- fragment.forEach((child) => {
160
- pieces.push(child.isInline ? inlineText(child) : blockText(child));
161
- });
162
- return pieces.join(fragment.firstChild?.isInline === true ? "" : "\n");
163
- }
164
31
  function bareWrappers(content, openStart, openEnd) {
165
32
  const wrapper = content.firstChild;
166
33
  if (openStart <= 1 || openEnd <= 1 || content.childCount !== 1)
@@ -181,9 +48,6 @@ function copiedSlice(slice) {
181
48
  slice.openEnd
182
49
  );
183
50
  }
184
- function clipboardText(slice) {
185
- return fragmentText(slice.content);
186
- }
187
51
  function dragCopyModifier() {
188
52
  return /Mac|iP(hone|[oa]d)/.test(navigator.platform) ? "altKey" : "ctrlKey";
189
53
  }
@@ -198,7 +62,11 @@ function dropMoves(view, event) {
198
62
  function docxClipboard(options = {}) {
199
63
  const normalizers = options.normalizers ?? DEFAULT_NORMALIZERS;
200
64
  let copyToken = null;
201
- const serializer = new ClipboardSerializer(() => copyToken);
65
+ const serializer = clipboardSerializer((copy) => {
66
+ if (copyToken !== null) {
67
+ copy.firstElementChild?.setAttribute(INTERNAL_TOKEN_ATTRIBUTE, copyToken);
68
+ }
69
+ });
202
70
  let host = null;
203
71
  const parser = new DocxClipboardParser(
204
72
  options.readers ?? DEFAULT_READERS,
@@ -1,6 +1,8 @@
1
1
  import type { EditorState } from "prosemirror-state";
2
2
  import type { FormattingContext, ParagraphStyleOption } from "../../docx/formatting";
3
+ import type { PageGeometry } from "../../docx/pageGeometry";
3
4
  import type { ImageToInsert } from "../insertImage";
5
+ import type { HtmlSource } from "./source";
4
6
  /**
5
7
  * Everything a piece of HTML is read against.
6
8
  *
@@ -13,6 +15,8 @@ export interface HtmlReadContext {
13
15
  /** The document the markup is read into an element of */
14
16
  document: Document;
15
17
  formatting: FormattingContext;
18
+ /** The paper a block read into the document lands on, which is the width a pasted table takes */
19
+ geometry: PageGeometry;
16
20
  paragraphStyles: readonly ParagraphStyleOption[];
17
21
  numbering: {
18
22
  used: ReadonlySet<number>;
@@ -20,5 +24,11 @@ export interface HtmlReadContext {
20
24
  };
21
25
  /** The images already loaded for this read, by the token the markup carries */
22
26
  images: ReadonlyMap<string, ImageToInsert>;
27
+ /**
28
+ * The application that wrote the markup being read (`./source`). It is a property of the markup
29
+ * and not of the document, so a context built before any markup is in hand names no application
30
+ * and `readHtml` fills it in from what it is handed.
31
+ */
32
+ source: HtmlSource;
23
33
  }
24
34
  export declare function readContextOf(state: EditorState, document: Document, images?: ReadonlyMap<string, ImageToInsert>): HtmlReadContext;
@@ -1,17 +1,23 @@
1
1
  // src/editor/clipboard/readContext.ts
2
2
  import { numIdsIn } from "../commands/listCommands.js";
3
- import { documentFormatting, documentParagraphStyles } from "../documentStyles.js";
3
+ import {
4
+ documentFormatting,
5
+ documentParagraphStyles,
6
+ sectionGeometryAt
7
+ } from "../documentStyles.js";
4
8
  import { canStartNewList } from "../plugins/numberingDecorations.js";
5
9
  function readContextOf(state, document, images = /* @__PURE__ */ new Map()) {
6
10
  return {
7
11
  document,
8
12
  formatting: documentFormatting(state),
13
+ geometry: sectionGeometryAt(state, state.selection.from),
9
14
  paragraphStyles: documentParagraphStyles(state),
10
15
  numbering: {
11
16
  used: numIdsIn(state.doc),
12
17
  canCreate: canStartNewList(state)
13
18
  },
14
- images
19
+ images,
20
+ source: "unknown"
15
21
  };
16
22
  }
17
23
  export {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Which application wrote the markup on the clipboard.
3
+ *
4
+ * Every writer of HTML says so somewhere - in a generator meta element, in a class prefix, in the
5
+ * id it wraps a copy in - and what it says decides which readings apply: Word's list paragraphs
6
+ * are lists rather than paragraphs beginning with a bullet character, and its `<o:p>` markers are
7
+ * nothing at all. A reading recognizing no writer stays with what the markup itself states.
8
+ */
9
+ /** The writers whose markup is read differently from plain HTML */
10
+ export type HtmlSource = "editor" | "word" | "google-docs" | "libreoffice" | "unknown";
11
+ /** The application the markup under this element was written by */
12
+ export declare function detectHtmlSource(root: ParentNode): HtmlSource;
@@ -0,0 +1,21 @@
1
+ // src/editor/clipboard/source.ts
2
+ import { editorClassNames } from "../../styles/classNames.js";
3
+ import { INTERNAL_TOKEN_ATTRIBUTE } from "./internalChannel.js";
4
+ var SIGNATURES = [
5
+ ["editor", `[${INTERNAL_TOKEN_ATTRIBUTE}], .${editorClassNames.paragraph}`],
6
+ ["word", 'meta[name="Generator" i][content^="Microsoft Word" i], .MsoNormal'],
7
+ ["google-docs", '[id^="docs-internal-guid-"]'],
8
+ [
9
+ "libreoffice",
10
+ 'meta[name="generator" i][content*="LibreOffice" i], meta[name="generator" i][content*="OpenOffice" i]'
11
+ ]
12
+ ];
13
+ function detectHtmlSource(root) {
14
+ for (const [source, selector] of SIGNATURES) {
15
+ if (root.querySelector(selector) !== null) return source;
16
+ }
17
+ return "unknown";
18
+ }
19
+ export {
20
+ detectHtmlSource
21
+ };
@@ -14,6 +14,7 @@ import {
14
14
  } from "../../../docx/story.js";
15
15
  import { docxSchema } from "../../../schema/index.js";
16
16
  import { guardedCommand } from "../../../schema/guards.js";
17
+ import { isWrapperType } from "../../../schema/wrappers.js";
17
18
  import {
18
19
  reservedCommentIds,
19
20
  reservedCommentParaIds
@@ -93,7 +94,7 @@ function wrapperMarksAt(state, pos, side) {
93
94
  const resolved = state.doc.resolve(pos);
94
95
  const adjacent = side === "before" ? resolved.nodeBefore : resolved.nodeAfter;
95
96
  return (adjacent?.marks ?? resolved.marks()).filter(
96
- (mark) => mark.type.name === "sdt" || mark.type.name === "link"
97
+ (mark) => isWrapperType(mark.type)
97
98
  );
98
99
  }
99
100
  function addCommentTransaction(state, comment, at) {
@@ -1,6 +1,7 @@
1
1
  // src/editor/commands/linkCommands.ts
2
2
  import { docxSchema } from "../../schema/index.js";
3
3
  import { openStretches } from "../../schema/guards.js";
4
+ import { innermostDepth, wrapperMarks, wrapperOf } from "../../schema/wrappers.js";
4
5
  var linkType = docxSchema.marks.link;
5
6
  function text(value) {
6
7
  return typeof value === "string" ? value : null;
@@ -16,6 +17,15 @@ function settled(address) {
16
17
  function linkMarkOf(marks) {
17
18
  return marks.find((mark) => mark.type === linkType) ?? null;
18
19
  }
20
+ function linkOf(node) {
21
+ return wrapperOf(node, linkType);
22
+ }
23
+ function sameLink(a, b) {
24
+ return atDepth(a, 0).eq(atDepth(b, 0));
25
+ }
26
+ function atDepth(mark, depth) {
27
+ return mark.attrs.depth === depth ? mark : mark.type.create({ ...mark.attrs, depth });
28
+ }
19
29
  function linkable(node, parent) {
20
30
  if (parent === null || !parent.type.allowsMarkType(linkType)) return false;
21
31
  return node.isText || node.type.allowsMarkType(linkType);
@@ -28,7 +38,10 @@ function piecesIn(doc, from, to) {
28
38
  const start = Math.max(pos, from);
29
39
  const end = Math.min(pos + node.nodeSize, to);
30
40
  if (start < end) {
31
- pieces.push({ from: start, to: end, link: linkMarkOf(node.marks) });
41
+ const link = linkOf(node);
42
+ pieces.push(
43
+ link === null ? { from: start, to: end, link, wrappers: wrapperMarks(node) } : { from: start, to: end, link }
44
+ );
32
45
  }
33
46
  return false;
34
47
  });
@@ -37,12 +50,12 @@ function piecesIn(doc, from, to) {
37
50
  function linkSpans(parent, start) {
38
51
  const spans = [];
39
52
  parent.forEach((child, offset) => {
40
- const link = linkMarkOf(child.marks);
53
+ const link = linkOf(child);
41
54
  if (!link) return;
42
55
  const from = start + offset;
43
56
  const to = from + child.nodeSize;
44
57
  const last = spans.at(-1);
45
- if (last && last.to === from && last.link.eq(link)) last.to = to;
58
+ if (last && last.to === from && sameLink(last.link, link)) last.to = to;
46
59
  else spans.push({ from, to, link });
47
60
  });
48
61
  return spans;
@@ -52,7 +65,7 @@ function caretSpan(state) {
52
65
  const link = linkMarkOf($from.marks());
53
66
  if (!link) return null;
54
67
  return linkSpans($from.parent, $from.start()).find(
55
- (span) => span.link.eq(link) && span.from <= $from.pos && $from.pos <= span.to
68
+ (span) => sameLink(span.link, link) && span.from <= $from.pos && $from.pos <= span.to
56
69
  ) ?? null;
57
70
  }
58
71
  function selectionSpan(state) {
@@ -93,7 +106,9 @@ function selectionPieces(state) {
93
106
  ]);
94
107
  }
95
108
  function linkMarkFor(piece, href) {
96
- return linkType.create({ ...piece.link?.attrs, href });
109
+ if (piece.link !== null)
110
+ return linkType.create({ ...piece.link.attrs, href });
111
+ return linkType.create({ href, depth: innermostDepth(piece.wrappers) + 1 });
97
112
  }
98
113
  function setLink(url) {
99
114
  return (state, dispatch) => {