@quillmark/svelte 0.25.0 → 0.26.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/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
|
+
}
|
package/dist/preview/viewport.js
CHANGED
|
@@ -13,6 +13,9 @@ const WHEEL_RATE = [0.0025, 0.08, 0.25];
|
|
|
13
13
|
* rate that inverts the pinch's encoding. */
|
|
14
14
|
const PINCH_DELTA = 50;
|
|
15
15
|
const PINCH_RATE = 0.01;
|
|
16
|
+
/** How far, as a log of the spread's ratio, two fingers part or close before a touch
|
|
17
|
+
* pinch zooms rather than only pans. */
|
|
18
|
+
const PINCH_SLOP = 0.04;
|
|
16
19
|
const px = (value) => parseFloat(value) || 0;
|
|
17
20
|
export function createViewport(container, slots, setScale, resetLabel) {
|
|
18
21
|
let zoom;
|
|
@@ -154,14 +157,13 @@ export function createViewport(container, slots, setScale, resetLabel) {
|
|
|
154
157
|
const factor = Math.exp(-e.deltaY * (pinched ? PINCH_RATE : WHEEL_RATE[e.deltaMode]));
|
|
155
158
|
glideTo((g) => g.raw * factor, e.clientX, e.clientY);
|
|
156
159
|
}
|
|
157
|
-
// The keys
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// a host's own handler took is left to it.
|
|
160
|
+
// The keys are the focused pane's alone: elsewhere they are the browser's own zoom,
|
|
161
|
+
// the pointer over the pane notwithstanding. A key a host's own handler took is left
|
|
162
|
+
// to it.
|
|
161
163
|
function onKeyDown(e) {
|
|
162
164
|
if (!(e.ctrlKey || e.metaKey) || e.altKey || e.defaultPrevented || e.isComposing)
|
|
163
165
|
return;
|
|
164
|
-
if (!
|
|
166
|
+
if (!container.contains(document.activeElement))
|
|
165
167
|
return;
|
|
166
168
|
const dir = e.key === '=' || e.key === '+' ? 1 : e.key === '-' || e.key === '_' ? -1 : 0;
|
|
167
169
|
if ((!dir && e.key !== '0') || !bounds())
|
|
@@ -169,30 +171,54 @@ export function createViewport(container, slots, setScale, resetLabel) {
|
|
|
169
171
|
e.preventDefault();
|
|
170
172
|
step((b) => (dir ? stepZoom(b.current, dir, b.floor) : undefined));
|
|
171
173
|
}
|
|
172
|
-
// A two-finger pinch on a touch screen. The browser's own page zoom is held off by the
|
|
173
|
-
// desk's `touch-action` and by cancelling the move, which also stops the pan a second
|
|
174
|
-
// finger would otherwise start.
|
|
175
174
|
let pinch;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
// The fingers on this pane, not one resting on the editor beside it.
|
|
176
|
+
const fingers = (e) => [...e.touches].filter((t) => t.target instanceof Node && container.contains(t.target));
|
|
177
|
+
const spread = ([a, b]) => Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
178
|
+
const midpoint = ([a, b]) => ({
|
|
179
|
+
x: (a.clientX + b.clientX) / 2,
|
|
180
|
+
y: (a.clientY + b.clientY) / 2
|
|
181
|
+
});
|
|
182
|
+
function grip(at) {
|
|
183
|
+
const page = pageAt(at.y);
|
|
184
|
+
const r = page?.getBoundingClientRect();
|
|
185
|
+
if (!page || !r?.width || !r.height)
|
|
186
|
+
return undefined;
|
|
187
|
+
return { page, fx: (at.x - r.left) / r.width, fy: (at.y - r.top) / r.height };
|
|
188
|
+
}
|
|
189
|
+
// A finger landing or lifting starts the pinch over with the two that remain.
|
|
190
|
+
function onTouches(e) {
|
|
191
|
+
pinch = undefined;
|
|
192
|
+
const t = fingers(e);
|
|
180
193
|
const b = bounds();
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
const held = t.length === 2 && b ? grip(midpoint(t)) : undefined;
|
|
195
|
+
if (held)
|
|
196
|
+
pinch = { spread: spread(t), raw: from(b).raw, zooming: false, ...held };
|
|
183
197
|
}
|
|
184
198
|
function onTouchMove(e) {
|
|
185
|
-
|
|
199
|
+
const t = fingers(e);
|
|
200
|
+
if (!pinch || t.length !== 2)
|
|
186
201
|
return;
|
|
187
202
|
e.preventDefault();
|
|
188
|
-
const
|
|
189
|
-
const ratio = spread(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
pinch =
|
|
203
|
+
const mid = midpoint(t);
|
|
204
|
+
const ratio = spread(t) / pinch.spread;
|
|
205
|
+
if (pinch.zooming) {
|
|
206
|
+
const start = pinch.raw;
|
|
207
|
+
glideTo(() => start * ratio, mid.x, mid.y);
|
|
208
|
+
}
|
|
209
|
+
else if (Math.abs(Math.log(ratio)) >= PINCH_SLOP) {
|
|
210
|
+
pinch.zooming = true;
|
|
211
|
+
pinch.spread = spread(t);
|
|
212
|
+
}
|
|
213
|
+
// A move the browser is already scrolling cannot be cancelled: its pan stands, and
|
|
214
|
+
// the pinch holds whatever is under the fingers now.
|
|
215
|
+
if (!e.cancelable) {
|
|
216
|
+
Object.assign(pinch, grip(mid));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const r = pinch.page.getBoundingClientRect();
|
|
220
|
+
container.scrollLeft += r.left + pinch.fx * r.width - mid.x;
|
|
221
|
+
container.scrollTop += r.top + pinch.fy * r.height - mid.y;
|
|
196
222
|
}
|
|
197
223
|
// Safari's trackpad pinch. iOS sends these beside the touches, which are the ones read
|
|
198
224
|
// there; either way the page zoom is cancelled.
|
|
@@ -231,10 +257,10 @@ export function createViewport(container, slots, setScale, resetLabel) {
|
|
|
231
257
|
const listeners = [
|
|
232
258
|
[container, 'wheel', onWheel, { passive: false }],
|
|
233
259
|
[window, 'keydown', onKeyDown],
|
|
234
|
-
[container, 'touchstart',
|
|
260
|
+
[container, 'touchstart', onTouches, { passive: true }],
|
|
235
261
|
[container, 'touchmove', onTouchMove, { passive: false }],
|
|
236
|
-
[container, 'touchend',
|
|
237
|
-
[container, 'touchcancel',
|
|
262
|
+
[container, 'touchend', onTouches],
|
|
263
|
+
[container, 'touchcancel', onTouches],
|
|
238
264
|
[container, 'gesturestart', onGestureStart],
|
|
239
265
|
[container, 'gesturechange', onGestureChange],
|
|
240
266
|
[container, 'gestureend', onGestureEnd],
|