@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.
package/dist/core/codec/field.js
CHANGED
|
@@ -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
|
|
14
|
-
// being one kind (`blocks.ts` §`listAtItem`)
|
|
15
|
-
//
|
|
16
|
-
// the
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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,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
|
+
}
|