@quillmark/svelte 0.25.0 → 0.26.0

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.
@@ -36,6 +36,7 @@ import { linkPastePlugin } from './link-paste.js';
36
36
  import { linePastePlugin } from './line-paste.js';
37
37
  import { listPastePlugin } from './list-paste.js';
38
38
  import { markdownPastePlugin } from './markdown-paste.js';
39
+ import { wordPastePlugin } from './word-paste.js';
39
40
  import { nestingPlugin } from './nesting.js';
40
41
  import { undoStepPlugin } from './undo.js';
41
42
  import { tablePastePlugin } from './table-paste.js';
@@ -572,7 +573,7 @@ export function proseLeafPlugins(schema, opts) {
572
573
  // Each answers to something only the block schema holds: any other leaf is
573
574
  // textblocks alone, with no island and no gap to put a cursor in.
574
575
  if (isBlockSchema(schema))
575
- list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), tableCopyPlugin(), markdownPastePlugin(), listPastePlugin(), taskPlugin(), nestingPlugin());
576
+ list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), tableCopyPlugin(), markdownPastePlugin(), wordPastePlugin(), listPastePlugin(), taskPlugin(), nestingPlugin());
576
577
  if (schema === plainSchema)
577
578
  list.push(plainClipboardPlugin());
578
579
  if (isInlineSchema(schema))
@@ -10,10 +10,11 @@
10
10
  // Otherwise each line becomes the kind: a run of plain lines is wrapped, one item a
11
11
  // line, and a run of continuation lines the same way where it stands, the first items
12
12
  // of a sub-list under their item, as `- ` or `1. ` at a continuation's head opens one
13
- // (`blocks.ts` §`openingAnItem`); a list of the other kind is retyped whole, a list
14
- // being one kind (`blocks.ts` §`listAtItem`); a line already in the kind stays. What it
15
- // wraps or retypes joins the lists of its kind it leaves touching (`touching.ts`), so
16
- // the lines of one selection count as one list.
13
+ // (`blocks.ts` §`openingAnItem`); the selected items of a list of the other kind split
14
+ // out of it as a list of the kind, a list being one kind (`blocks.ts` §`listAtItem`),
15
+ // its tail counting from 1 as `unlist` leaves one; a line already in the kind stays.
16
+ // What it wraps or retypes joins the lists of its kind it leaves touching
17
+ // (`touching.ts`), so the lines of one selection count as one list.
17
18
  import { Fragment, NodeRange, Slice } from 'prosemirror-model';
18
19
  import { wrapRangeInList } from 'prosemirror-schema-list';
19
20
  import { ReplaceAroundStep } from 'prosemirror-transform';
@@ -108,20 +109,41 @@ function unlist(tr, type, itemType) {
108
109
  tr.setNodeMarkup(tail, null, { ...rest.attrs, start: 1 });
109
110
  }
110
111
  }
112
+ /** Items `first` to `last` of the list at `pos` split out as a list of their own,
113
+ * the tail after them counting from 1. Returns where that list stands. */
114
+ function splitOut(tr, pos, first, last) {
115
+ const list = tr.doc.nodeAt(pos);
116
+ const $inside = tr.doc.resolve(pos + 1);
117
+ if (last + 1 < list.childCount) {
118
+ tr.split($inside.posAtIndex(last + 1));
119
+ const tail = tr.doc.resolve(pos + 1).posAtIndex(last + 1) + 1;
120
+ if (list.attrs.start != null && list.attrs.start !== 1)
121
+ tr.setNodeMarkup(tail, null, { ...list.attrs, start: 1 });
122
+ }
123
+ if (first === 0)
124
+ return pos;
125
+ tr.split($inside.posAtIndex(first));
126
+ return $inside.posAtIndex(first) + 1;
127
+ }
111
128
  /** Make every line the kind, and join what that leaves touching. */
112
129
  function relist(tr, lines, type, paragraph) {
113
130
  const changed = [];
114
131
  const runs = [];
132
+ const others = new Map();
115
133
  for (const line of lines) {
116
134
  const { $at, item } = line;
117
135
  if (line.covered)
118
136
  continue;
119
137
  if (line.opens) {
138
+ if ($at.node(item - 1).type === type)
139
+ continue;
120
140
  const pos = $at.before(item - 1);
121
- if ($at.node(item - 1).type !== type && !changed.some((c) => c.pos === pos)) {
122
- tr.setNodeMarkup(pos, type, type.name === 'ordered_list' ? { start: 1 } : null);
123
- changed.push({ pos, step: tr.steps.length });
124
- }
141
+ const index = $at.index(item - 1);
142
+ const span = others.get(pos);
143
+ if (span)
144
+ span.last = index;
145
+ else
146
+ others.set(pos, { first: index, last: index });
125
147
  continue;
126
148
  }
127
149
  const end = line.pos + $at.parent.nodeSize;
@@ -138,6 +160,11 @@ function relist(tr, lines, type, paragraph) {
138
160
  return false;
139
161
  changed.push({ pos: run.from, step: tr.steps.length });
140
162
  }
163
+ for (const [pos, span] of [...others].reverse()) {
164
+ const at = splitOut(tr, tr.mapping.map(pos), span.first, span.last);
165
+ tr.setNodeMarkup(at, type, type.name === 'ordered_list' ? { start: 1 } : null);
166
+ changed.push({ pos: at, step: tr.steps.length });
167
+ }
141
168
  const seams = [];
142
169
  for (const { pos, step } of changed) {
143
170
  const at = tr.mapping.slice(step).map(pos);
@@ -0,0 +1,5 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ /** `html` with every run of Word list paragraphs made a list; `html` itself where there
3
+ * are none. */
4
+ export declare function wordLists(html: string): string;
5
+ export declare function wordPastePlugin(): Plugin;
@@ -0,0 +1,87 @@
1
+ // A list copied out of Word or Outlook lands as a list. Desktop Word writes no `<ol>` or
2
+ // `<li>`: each item is a paragraph styled `mso-list:l0 level2 lfo1`, its marker drawn by a
3
+ // `mso-list:Ignore` span ahead of the text. Each run of those paragraphs is rebuilt here as
4
+ // the nested lists it draws, the marker span dropped, so the paste rules after this one
5
+ // (`list-paste.ts`) see a list as any other source writes one.
6
+ import { Plugin } from 'prosemirror-state';
7
+ /** A Word list paragraph's style: the list it is in, and its level from 1. */
8
+ const MSO_LIST = /mso-list:\s*(l\d+)\s+level(\d+)\s+(lfo\d+)/i;
9
+ /** A numbered marker: digits, or letters, closed by `.` or `)`, or in parentheses, and a
10
+ * legal outline's `1.2.` whose last part is the item's own. Every other marker is a
11
+ * bullet, Word's `·`, `o` and `§` among them. */
12
+ const ORDINAL = /^\(?(?:\d+\.)*(\d+|[a-z]+)[.)]$/i;
13
+ /** The marker text of a list paragraph, its span taken out. */
14
+ function takeMarker(p) {
15
+ const span = [...p.querySelectorAll('span')].find((s) => /mso-list:\s*Ignore/i.test(s.getAttribute('style') ?? ''));
16
+ if (!span)
17
+ return '';
18
+ const marker = (span.textContent ?? '').replace(/\s+/g, '');
19
+ span.remove();
20
+ return marker;
21
+ }
22
+ /** `run`'s paragraphs as nested lists, one level of the stack a level of Word's. */
23
+ function rebuild(doc, run) {
24
+ const stack = [];
25
+ let root = null;
26
+ for (const { p, level } of run) {
27
+ const marker = takeMarker(p);
28
+ while (stack.length && stack[stack.length - 1].level > level)
29
+ stack.pop();
30
+ const top = stack[stack.length - 1];
31
+ if (!top || top.level < level) {
32
+ const ordinal = ORDINAL.exec(marker);
33
+ const list = doc.createElement(ordinal ? 'ol' : 'ul');
34
+ if (ordinal && /^\d+$/.test(ordinal[1]) && ordinal[1] !== '1')
35
+ list.setAttribute('start', ordinal[1]);
36
+ if (top)
37
+ top.list.lastElementChild.append(list);
38
+ else
39
+ root = list;
40
+ stack.push({ level, list });
41
+ }
42
+ const item = doc.createElement('li');
43
+ p.removeAttribute('style');
44
+ p.removeAttribute('class');
45
+ item.append(p);
46
+ stack[stack.length - 1].list.append(item);
47
+ }
48
+ return root;
49
+ }
50
+ /** `html` with every run of Word list paragraphs made a list; `html` itself where there
51
+ * are none. */
52
+ export function wordLists(html) {
53
+ if (!/mso-list/i.test(html))
54
+ return html;
55
+ const doc = new DOMParser().parseFromString(html, 'text/html');
56
+ const runs = [];
57
+ let id = null;
58
+ for (const p of doc.body.querySelectorAll('p')) {
59
+ const match = MSO_LIST.exec(p.getAttribute('style') ?? '');
60
+ if (!match)
61
+ continue;
62
+ const prev = runs[runs.length - 1]?.at(-1)?.p;
63
+ let sibling = p.previousSibling;
64
+ while (sibling &&
65
+ sibling.nodeType !== 1 &&
66
+ !(sibling.nodeType === 3 && sibling.textContent.trim()))
67
+ sibling = sibling.previousSibling;
68
+ const list = match[1] + match[3];
69
+ const entry = { p, level: Number(match[2]) };
70
+ if (prev && sibling === prev && list === id)
71
+ runs[runs.length - 1].push(entry);
72
+ else
73
+ runs.push([entry]);
74
+ id = list;
75
+ }
76
+ if (!runs.length)
77
+ return html;
78
+ for (const run of runs) {
79
+ const anchor = doc.createComment('');
80
+ run[0].p.before(anchor);
81
+ anchor.replaceWith(rebuild(doc, run));
82
+ }
83
+ return doc.body.innerHTML;
84
+ }
85
+ export function wordPastePlugin() {
86
+ return new Plugin({ props: { transformPastedHTML: (html) => wordLists(html) } });
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
5
5
  "repository": {
6
6
  "type": "git",