@aicayzer/inkkit 0.0.14 → 0.0.16
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/NOTICE +0 -4
- package/README.md +27 -24
- package/dist/comments.js +7 -0
- package/dist/dialect.js +10 -19
- package/dist/editor.d.ts +3 -0
- package/dist/editor.js +52 -27
- package/dist/highlight.js +109 -22
- package/dist/image-blocks.d.ts +71 -0
- package/dist/image-blocks.js +286 -0
- package/dist/images.d.ts +3 -1
- package/dist/images.js +52 -23
- package/dist/mermaid.js +108 -6
- package/dist/paste-html.d.ts +23 -0
- package/dist/paste-html.js +441 -0
- package/dist/paste.js +39 -52
- package/dist/preserve.d.ts +1 -0
- package/dist/preserve.js +87 -26
- package/dist/search.d.ts +3 -3
- package/dist/search.js +2 -2
- package/dist/style.css +98 -4
- package/dist/text-ranges.js +10 -1
- package/package.json +1 -1
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// Elements the HTML paste path hands to ProseMirror's parser. Anything else
|
|
2
|
+
// is kept as literal source, so a tag must be listed here or rewritten by
|
|
3
|
+
// `normalizeClipboardHTML` to avoid becoming a raw-HTML block.
|
|
4
|
+
const rich = new Set([
|
|
5
|
+
'P',
|
|
6
|
+
'H1',
|
|
7
|
+
'H2',
|
|
8
|
+
'H3',
|
|
9
|
+
'H4',
|
|
10
|
+
'H5',
|
|
11
|
+
'H6',
|
|
12
|
+
'STRONG',
|
|
13
|
+
'MARK',
|
|
14
|
+
'B',
|
|
15
|
+
'EM',
|
|
16
|
+
'I',
|
|
17
|
+
'S',
|
|
18
|
+
'DEL',
|
|
19
|
+
'STRIKE',
|
|
20
|
+
'CODE',
|
|
21
|
+
'PRE',
|
|
22
|
+
'BLOCKQUOTE',
|
|
23
|
+
'A',
|
|
24
|
+
'BR',
|
|
25
|
+
'HR',
|
|
26
|
+
'UL',
|
|
27
|
+
'OL',
|
|
28
|
+
'LI',
|
|
29
|
+
'TABLE',
|
|
30
|
+
'THEAD',
|
|
31
|
+
'TBODY',
|
|
32
|
+
'TFOOT',
|
|
33
|
+
'TR',
|
|
34
|
+
'TH',
|
|
35
|
+
'TD',
|
|
36
|
+
'IMG',
|
|
37
|
+
'SPAN',
|
|
38
|
+
'SUP',
|
|
39
|
+
'DIV',
|
|
40
|
+
'FONT',
|
|
41
|
+
'SECTION',
|
|
42
|
+
'ARTICLE',
|
|
43
|
+
'MAIN',
|
|
44
|
+
'HEADER',
|
|
45
|
+
'FOOTER',
|
|
46
|
+
]);
|
|
47
|
+
// Document furniture and form inputs hold no content of their own. A
|
|
48
|
+
// template never keeps `head`, so it is not listed.
|
|
49
|
+
const dropped = new Set(['STYLE', 'META', 'LINK', 'TITLE', 'SOURCE', 'INPUT']);
|
|
50
|
+
// The dialect has no underline, subscript or similar marks, so these keep
|
|
51
|
+
// their children and lose only the wrapper.
|
|
52
|
+
const unwrapped = new Set([
|
|
53
|
+
'U',
|
|
54
|
+
'SUB',
|
|
55
|
+
'SMALL',
|
|
56
|
+
'TIME',
|
|
57
|
+
'ABBR',
|
|
58
|
+
'CITE',
|
|
59
|
+
'DFN',
|
|
60
|
+
'VAR',
|
|
61
|
+
'INS',
|
|
62
|
+
'BDI',
|
|
63
|
+
'BDO',
|
|
64
|
+
'LABEL',
|
|
65
|
+
]);
|
|
66
|
+
const blockWrappers = new Set(['FIGURE', 'FIGCAPTION', 'DL', 'DD']);
|
|
67
|
+
const handled = new Set([
|
|
68
|
+
...dropped,
|
|
69
|
+
...unwrapped,
|
|
70
|
+
...blockWrappers,
|
|
71
|
+
'DT',
|
|
72
|
+
'PICTURE',
|
|
73
|
+
]);
|
|
74
|
+
export function isRichElement(element) {
|
|
75
|
+
return rich.has(element.tagName);
|
|
76
|
+
}
|
|
77
|
+
/** Whether normalisation enters the element, so its content may be rewritten. */
|
|
78
|
+
export function isRewrittenElement(element) {
|
|
79
|
+
return rich.has(element.tagName) || handled.has(element.tagName);
|
|
80
|
+
}
|
|
81
|
+
// `white-space` and its longhand `white-space-collapse` keep source spaces with
|
|
82
|
+
// these keywords. `pre-line` and `preserve-breaks` collapse spaces, so they are
|
|
83
|
+
// collapsed here too.
|
|
84
|
+
const keepingWhitespace = new Set([
|
|
85
|
+
'pre',
|
|
86
|
+
'pre-wrap',
|
|
87
|
+
'break-spaces',
|
|
88
|
+
'preserve',
|
|
89
|
+
'preserve-spaces',
|
|
90
|
+
]);
|
|
91
|
+
/**
|
|
92
|
+
* Whether the element's own inline style makes a browser keep the whitespace
|
|
93
|
+
* of its content, as copied editor code does with `white-space: pre`. The
|
|
94
|
+
* declarations are parsed, so case, spacing, comments, quoted text and
|
|
95
|
+
* `!important` do not matter. Style sheets are not consulted.
|
|
96
|
+
*/
|
|
97
|
+
export function keepsWhitespace(element) {
|
|
98
|
+
const style = element.getAttribute('style');
|
|
99
|
+
if (!style)
|
|
100
|
+
return false;
|
|
101
|
+
const winning = new Map();
|
|
102
|
+
for (const declaration of declarations(style)) {
|
|
103
|
+
const colon = declaration.indexOf(':');
|
|
104
|
+
if (colon < 0)
|
|
105
|
+
continue;
|
|
106
|
+
const name = declaration.slice(0, colon).trim().toLowerCase();
|
|
107
|
+
if (name !== 'white-space' && name !== 'white-space-collapse')
|
|
108
|
+
continue;
|
|
109
|
+
let value = declaration.slice(colon + 1).trim();
|
|
110
|
+
const important = /!\s*important$/i.test(value);
|
|
111
|
+
if (important)
|
|
112
|
+
value = value.replace(/!\s*important$/i, '');
|
|
113
|
+
const earlier = winning.get(name);
|
|
114
|
+
if (!earlier?.important || important)
|
|
115
|
+
winning.set(name, { value: value.trim().toLowerCase(), important });
|
|
116
|
+
}
|
|
117
|
+
return [...winning.values()].some(({ value }) => value.split(/\s+/).some((keyword) => keepingWhitespace.has(keyword)));
|
|
118
|
+
}
|
|
119
|
+
// Splits a style attribute at the semicolons that end declarations, ignoring
|
|
120
|
+
// those inside comments, quoted strings and parentheses such as `url(...)`.
|
|
121
|
+
function declarations(style) {
|
|
122
|
+
const result = [];
|
|
123
|
+
let current = '';
|
|
124
|
+
let quote = '';
|
|
125
|
+
let depth = 0;
|
|
126
|
+
for (let index = 0; index < style.length; index++) {
|
|
127
|
+
const character = style[index];
|
|
128
|
+
if (quote) {
|
|
129
|
+
current += character;
|
|
130
|
+
if (character === '\\' && index + 1 < style.length)
|
|
131
|
+
current += style[++index];
|
|
132
|
+
else if (character === quote)
|
|
133
|
+
quote = '';
|
|
134
|
+
}
|
|
135
|
+
else if (character === '/' && style[index + 1] === '*') {
|
|
136
|
+
const end = style.indexOf('*/', index + 2);
|
|
137
|
+
index = end < 0 ? style.length : end + 1;
|
|
138
|
+
}
|
|
139
|
+
else if (character === '"' || character === "'") {
|
|
140
|
+
quote = character;
|
|
141
|
+
current += character;
|
|
142
|
+
}
|
|
143
|
+
else if (character === '(') {
|
|
144
|
+
depth++;
|
|
145
|
+
current += character;
|
|
146
|
+
}
|
|
147
|
+
else if (character === ')') {
|
|
148
|
+
depth = Math.max(0, depth - 1);
|
|
149
|
+
current += character;
|
|
150
|
+
}
|
|
151
|
+
else if (character === ';' && depth === 0) {
|
|
152
|
+
result.push(current);
|
|
153
|
+
current = '';
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
current += character;
|
|
157
|
+
}
|
|
158
|
+
result.push(current);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Rewrites common web-page elements to ones the importer already understands.
|
|
163
|
+
* Elements that will stay literal source are not entered, so their content is
|
|
164
|
+
* preserved exactly.
|
|
165
|
+
*/
|
|
166
|
+
export function normalizeClipboardHTML(root) {
|
|
167
|
+
for (const node of [...root.childNodes]) {
|
|
168
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
169
|
+
// Windows and some native apps leave the CF_HTML fragment markers in.
|
|
170
|
+
if (/^\s*(?:Start|End)Fragment\s*$/.test(node.data))
|
|
171
|
+
node.parentNode?.removeChild(node);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
175
|
+
continue;
|
|
176
|
+
const element = node;
|
|
177
|
+
const tag = element.tagName;
|
|
178
|
+
if (!rich.has(tag) && !handled.has(tag))
|
|
179
|
+
continue;
|
|
180
|
+
if (dropped.has(tag)) {
|
|
181
|
+
const parent = element.parentElement;
|
|
182
|
+
element.remove();
|
|
183
|
+
if (parent)
|
|
184
|
+
removeEmptyParagraph(parent);
|
|
185
|
+
}
|
|
186
|
+
else if (tag === 'PICTURE') {
|
|
187
|
+
const image = element.querySelector('img');
|
|
188
|
+
if (image)
|
|
189
|
+
element.replaceWith(image);
|
|
190
|
+
else
|
|
191
|
+
element.remove();
|
|
192
|
+
}
|
|
193
|
+
else if (unwrapped.has(tag)) {
|
|
194
|
+
normalizeClipboardHTML(element);
|
|
195
|
+
element.replaceWith(...element.childNodes);
|
|
196
|
+
}
|
|
197
|
+
else if (tag === 'DT') {
|
|
198
|
+
normalizeClipboardHTML(element);
|
|
199
|
+
// Markdown has no definition lists. A bold term over its definition
|
|
200
|
+
// keeps which text names the other.
|
|
201
|
+
const term = element.ownerDocument.createElement('div');
|
|
202
|
+
boldInto(term, [...element.childNodes]);
|
|
203
|
+
element.replaceWith(term);
|
|
204
|
+
}
|
|
205
|
+
else if (blockWrappers.has(tag)) {
|
|
206
|
+
normalizeClipboardHTML(element);
|
|
207
|
+
const block = element.ownerDocument.createElement('div');
|
|
208
|
+
block.append(...element.childNodes);
|
|
209
|
+
element.replaceWith(block);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
if (tag === 'LI')
|
|
213
|
+
markTask(element);
|
|
214
|
+
normalizeClipboardHTML(element);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// A paragraph whose only content was a dropped element would otherwise import
|
|
219
|
+
// as a blank paragraph.
|
|
220
|
+
function removeEmptyParagraph(from) {
|
|
221
|
+
let node = from;
|
|
222
|
+
while (node &&
|
|
223
|
+
['SPAN', 'FONT', 'STRONG', 'B', 'EM', 'I'].includes(node.tagName))
|
|
224
|
+
node = node.parentElement;
|
|
225
|
+
if (node?.tagName === 'P' &&
|
|
226
|
+
!node.textContent?.trim() &&
|
|
227
|
+
!node.querySelector('img,br,hr'))
|
|
228
|
+
node.remove();
|
|
229
|
+
}
|
|
230
|
+
const termBlocks = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
|
|
231
|
+
const blocks = new Set([
|
|
232
|
+
...termBlocks,
|
|
233
|
+
'BLOCKQUOTE',
|
|
234
|
+
'PRE',
|
|
235
|
+
'UL',
|
|
236
|
+
'OL',
|
|
237
|
+
'LI',
|
|
238
|
+
'TABLE',
|
|
239
|
+
'THEAD',
|
|
240
|
+
'TBODY',
|
|
241
|
+
'TFOOT',
|
|
242
|
+
'TR',
|
|
243
|
+
'TH',
|
|
244
|
+
'TD',
|
|
245
|
+
'SECTION',
|
|
246
|
+
'ARTICLE',
|
|
247
|
+
'MAIN',
|
|
248
|
+
'HEADER',
|
|
249
|
+
'FOOTER',
|
|
250
|
+
'HR',
|
|
251
|
+
]);
|
|
252
|
+
// Bolds the inline content of a term. Paragraphs, headings and divisions
|
|
253
|
+
// inside it each become one bold paragraph; other blocks are kept as they are.
|
|
254
|
+
function boldInto(target, nodes) {
|
|
255
|
+
const document = target.ownerDocument;
|
|
256
|
+
let run;
|
|
257
|
+
const paragraph = (content) => {
|
|
258
|
+
if (!content.some((node) => node.textContent?.trim() || isImage(node)))
|
|
259
|
+
return undefined;
|
|
260
|
+
const block = document.createElement('p');
|
|
261
|
+
const strong = document.createElement('strong');
|
|
262
|
+
strong.append(...content);
|
|
263
|
+
block.append(strong);
|
|
264
|
+
target.append(block);
|
|
265
|
+
return strong;
|
|
266
|
+
};
|
|
267
|
+
for (const node of nodes) {
|
|
268
|
+
const element = node.nodeType === Node.ELEMENT_NODE ? node : null;
|
|
269
|
+
if (element && blocks.has(element.tagName)) {
|
|
270
|
+
run = undefined;
|
|
271
|
+
if (element.tagName === 'P' || /^H[1-6]$/.test(element.tagName))
|
|
272
|
+
paragraph([...element.childNodes]);
|
|
273
|
+
else if (element.tagName === 'DIV')
|
|
274
|
+
boldInto(target, [...element.childNodes]);
|
|
275
|
+
else
|
|
276
|
+
target.append(element);
|
|
277
|
+
}
|
|
278
|
+
else if (run)
|
|
279
|
+
run.append(node);
|
|
280
|
+
else if (node.textContent?.trim() || isImage(node)) {
|
|
281
|
+
run = paragraph([node]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function isImage(node) {
|
|
286
|
+
return (node.nodeType === Node.ELEMENT_NODE &&
|
|
287
|
+
(node.tagName === 'IMG' ||
|
|
288
|
+
!!node.querySelector('img')));
|
|
289
|
+
}
|
|
290
|
+
// GitHub, Notion and similar pages render a task as a checkbox leading its
|
|
291
|
+
// list item, optionally inside a paragraph or label.
|
|
292
|
+
function leadingCheckbox(item) {
|
|
293
|
+
let node = item;
|
|
294
|
+
for (;;) {
|
|
295
|
+
const first = [...node.childNodes].find((child) => child.nodeType !== Node.COMMENT_NODE &&
|
|
296
|
+
!(child.nodeType === Node.TEXT_NODE && !child.textContent?.trim()));
|
|
297
|
+
if (first?.nodeType !== Node.ELEMENT_NODE)
|
|
298
|
+
return undefined;
|
|
299
|
+
const element = first;
|
|
300
|
+
if (element.tagName === 'INPUT')
|
|
301
|
+
return element.getAttribute('type')?.toLowerCase() === 'checkbox'
|
|
302
|
+
? element
|
|
303
|
+
: undefined;
|
|
304
|
+
if (!['P', 'LABEL', 'DIV', 'SPAN'].includes(element.tagName))
|
|
305
|
+
return undefined;
|
|
306
|
+
node = element;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function markTask(item) {
|
|
310
|
+
const checkbox = leadingCheckbox(item);
|
|
311
|
+
if (!checkbox)
|
|
312
|
+
return;
|
|
313
|
+
item.setAttribute('data-item-type', 'task');
|
|
314
|
+
item.setAttribute('data-checked', String(checkbox.hasAttribute('checked')));
|
|
315
|
+
checkbox.remove();
|
|
316
|
+
}
|
|
317
|
+
const inline = new Set([
|
|
318
|
+
'STRONG',
|
|
319
|
+
'MARK',
|
|
320
|
+
'B',
|
|
321
|
+
'EM',
|
|
322
|
+
'I',
|
|
323
|
+
'S',
|
|
324
|
+
'DEL',
|
|
325
|
+
'STRIKE',
|
|
326
|
+
'A',
|
|
327
|
+
'SPAN',
|
|
328
|
+
'SUP',
|
|
329
|
+
'FONT',
|
|
330
|
+
]);
|
|
331
|
+
const space = /[ \t\r\n\f]+/g;
|
|
332
|
+
/**
|
|
333
|
+
* Collapses source whitespace the way a browser renders it: runs become one
|
|
334
|
+
* space, and none remains at the edges of a block or beside a line break.
|
|
335
|
+
* `pre`, `code`, elements that stay literal source and elements whose style
|
|
336
|
+
* keeps whitespace are not entered.
|
|
337
|
+
*/
|
|
338
|
+
export function collapseClipboardWhitespace(root) {
|
|
339
|
+
hoistEdgeSpace(root);
|
|
340
|
+
let trailing;
|
|
341
|
+
let start = true;
|
|
342
|
+
const trim = () => {
|
|
343
|
+
if (!trailing)
|
|
344
|
+
return;
|
|
345
|
+
trailing.data = trailing.data.slice(0, -1);
|
|
346
|
+
if (!trailing.data)
|
|
347
|
+
trailing.remove();
|
|
348
|
+
trailing = undefined;
|
|
349
|
+
};
|
|
350
|
+
const visit = (node) => {
|
|
351
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
352
|
+
const text = node;
|
|
353
|
+
let data = text.data.replace(space, ' ');
|
|
354
|
+
if ((start || trailing) && data.startsWith(' '))
|
|
355
|
+
data = data.slice(1);
|
|
356
|
+
if (!data)
|
|
357
|
+
return text.remove();
|
|
358
|
+
text.data = data;
|
|
359
|
+
start = false;
|
|
360
|
+
trailing = data.endsWith(' ') ? text : undefined;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
364
|
+
return;
|
|
365
|
+
const element = node;
|
|
366
|
+
const tag = element.tagName;
|
|
367
|
+
if (keepsWhitespace(element)) {
|
|
368
|
+
// Treated like `code` inside a line and like `pre` as a block.
|
|
369
|
+
if (inline.has(tag) || tag === 'CODE' || tag === 'IMG') {
|
|
370
|
+
trailing = undefined;
|
|
371
|
+
start = false;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
trim();
|
|
375
|
+
start = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else if (inline.has(tag)) {
|
|
379
|
+
for (const child of [...element.childNodes])
|
|
380
|
+
visit(child);
|
|
381
|
+
}
|
|
382
|
+
else if (tag === 'BR') {
|
|
383
|
+
trim();
|
|
384
|
+
start = true;
|
|
385
|
+
}
|
|
386
|
+
else if (tag === 'CODE' || tag === 'IMG') {
|
|
387
|
+
trailing = undefined;
|
|
388
|
+
start = false;
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
trim();
|
|
392
|
+
start = true;
|
|
393
|
+
if (blocks.has(tag) && tag !== 'PRE' && tag !== 'HR')
|
|
394
|
+
for (const child of [...element.childNodes])
|
|
395
|
+
visit(child);
|
|
396
|
+
trim();
|
|
397
|
+
start = true;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
for (const child of [...root.childNodes])
|
|
401
|
+
visit(child);
|
|
402
|
+
trim();
|
|
403
|
+
}
|
|
404
|
+
// Whitespace inside emphasis, links and similar wrappers cannot be written in
|
|
405
|
+
// Markdown, so it moves outside them.
|
|
406
|
+
function hoistEdgeSpace(node) {
|
|
407
|
+
for (const child of [...node.childNodes]) {
|
|
408
|
+
if (child.nodeType !== Node.ELEMENT_NODE)
|
|
409
|
+
continue;
|
|
410
|
+
const element = child;
|
|
411
|
+
if (keepsWhitespace(element))
|
|
412
|
+
continue;
|
|
413
|
+
if (inline.has(element.tagName)) {
|
|
414
|
+
hoistEdgeSpace(element);
|
|
415
|
+
const lead = edgeText(element, 'start');
|
|
416
|
+
const leading = lead?.data.match(/^[ \t\r\n\f]+/)?.[0];
|
|
417
|
+
if (lead && leading) {
|
|
418
|
+
lead.data = lead.data.slice(leading.length);
|
|
419
|
+
element.before(leading);
|
|
420
|
+
}
|
|
421
|
+
const tail = edgeText(element, 'end');
|
|
422
|
+
const trailing = tail?.data.match(/[ \t\r\n\f]+$/)?.[0];
|
|
423
|
+
if (tail && trailing) {
|
|
424
|
+
tail.data = tail.data.slice(0, -trailing.length);
|
|
425
|
+
element.after(trailing);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else if (blocks.has(element.tagName) && element.tagName !== 'PRE')
|
|
429
|
+
hoistEdgeSpace(element);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function edgeText(element, side) {
|
|
433
|
+
let node = side === 'start' ? element.firstChild : element.lastChild;
|
|
434
|
+
while (node && node.nodeType === Node.ELEMENT_NODE) {
|
|
435
|
+
if (!inline.has(node.tagName) ||
|
|
436
|
+
keepsWhitespace(node))
|
|
437
|
+
return undefined;
|
|
438
|
+
node = side === 'start' ? node.firstChild : node.lastChild;
|
|
439
|
+
}
|
|
440
|
+
return node?.nodeType === Node.TEXT_NODE ? node : undefined;
|
|
441
|
+
}
|
package/dist/paste.js
CHANGED
|
@@ -3,6 +3,8 @@ import { DOMParser as ProseParser, DOMSerializer, Fragment, Slice, } from '@milk
|
|
|
3
3
|
import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
|
|
4
4
|
import { $prose } from '@milkdown/kit/utils';
|
|
5
5
|
import { pasteSpreadsheet } from './spreadsheet.js';
|
|
6
|
+
import { isImage } from './image-blocks.js';
|
|
7
|
+
import { collapseClipboardWhitespace, isRewrittenElement, isRichElement, normalizeClipboardHTML, } from './paste-html.js';
|
|
6
8
|
import { avoidReferenceCollisions, markdownFromHTML, } from './reference-clipboard.js';
|
|
7
9
|
import { InkKitError, } from './types.js';
|
|
8
10
|
export class PasteController {
|
|
@@ -155,8 +157,25 @@ export class PasteController {
|
|
|
155
157
|
for (const table of [...container.querySelectorAll('table')]) {
|
|
156
158
|
if (table.parentElement?.closest('table'))
|
|
157
159
|
continue;
|
|
158
|
-
|
|
160
|
+
// Inside an element that stays literal source, the whole element is
|
|
161
|
+
// kept as written, so its tables are not rewritten or judged here.
|
|
162
|
+
if (!insideRewrittenContent(table))
|
|
159
163
|
continue;
|
|
164
|
+
if (schema.nodes.table) {
|
|
165
|
+
// Judge the table as it will import, so wrappers such as `u` or
|
|
166
|
+
// `label` in a cell do not make it literal. A table that is still
|
|
167
|
+
// unsupported keeps its original source. An input in a cell would
|
|
168
|
+
// be dropped with its state, so that table stays source too.
|
|
169
|
+
const trial = table.cloneNode(true);
|
|
170
|
+
const holder = table.ownerDocument.createDocumentFragment();
|
|
171
|
+
holder.append(trial);
|
|
172
|
+
normalizeClipboardHTML(holder);
|
|
173
|
+
if (!table.querySelector('td input,th input') &&
|
|
174
|
+
!unsupportedClipboardTable(trial)) {
|
|
175
|
+
table.replaceWith(trial);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
160
179
|
const preserved = ctx.get(parserCtx)(table.outerHTML);
|
|
161
180
|
table.replaceWith(DOMSerializer.fromSchema(schema).serializeFragment(preserved.content));
|
|
162
181
|
}
|
|
@@ -192,10 +211,10 @@ export class PasteController {
|
|
|
192
211
|
element.removeAttribute(name);
|
|
193
212
|
}
|
|
194
213
|
}
|
|
195
|
-
// Native HTML clipboards add encoding metadata
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
214
|
+
// Native HTML clipboards add encoding metadata and fragment markers.
|
|
215
|
+
normalizeClipboardHTML(container);
|
|
216
|
+
// Obsidian's reading view also copies its title and collapse controls
|
|
217
|
+
// with document HTML.
|
|
199
218
|
for (const control of container.querySelectorAll('div.mod-header.mod-ui,div.mod-footer.mod-ui')) {
|
|
200
219
|
if (control.querySelector('.inline-title[contenteditable],.embedded-backlinks'))
|
|
201
220
|
control.remove();
|
|
@@ -211,51 +230,8 @@ export class PasteController {
|
|
|
211
230
|
if (control.querySelector('svg.svg-icon'))
|
|
212
231
|
control.remove();
|
|
213
232
|
}
|
|
214
|
-
const supported = new Set([
|
|
215
|
-
'P',
|
|
216
|
-
'H1',
|
|
217
|
-
'H2',
|
|
218
|
-
'H3',
|
|
219
|
-
'H4',
|
|
220
|
-
'H5',
|
|
221
|
-
'H6',
|
|
222
|
-
'STRONG',
|
|
223
|
-
'MARK',
|
|
224
|
-
'B',
|
|
225
|
-
'EM',
|
|
226
|
-
'I',
|
|
227
|
-
'S',
|
|
228
|
-
'DEL',
|
|
229
|
-
'STRIKE',
|
|
230
|
-
'CODE',
|
|
231
|
-
'PRE',
|
|
232
|
-
'BLOCKQUOTE',
|
|
233
|
-
'A',
|
|
234
|
-
'BR',
|
|
235
|
-
'HR',
|
|
236
|
-
'UL',
|
|
237
|
-
'OL',
|
|
238
|
-
'LI',
|
|
239
|
-
'TABLE',
|
|
240
|
-
'THEAD',
|
|
241
|
-
'TBODY',
|
|
242
|
-
'TFOOT',
|
|
243
|
-
'TR',
|
|
244
|
-
'TH',
|
|
245
|
-
'TD',
|
|
246
|
-
'IMG',
|
|
247
|
-
'SPAN',
|
|
248
|
-
'SUP',
|
|
249
|
-
'DIV',
|
|
250
|
-
'FONT',
|
|
251
|
-
'SECTION',
|
|
252
|
-
'ARTICLE',
|
|
253
|
-
'MAIN',
|
|
254
|
-
'HEADER',
|
|
255
|
-
'FOOTER',
|
|
256
|
-
]);
|
|
257
233
|
for (const element of [...container.querySelectorAll('*')]) {
|
|
258
|
-
if (!container.contains(element) ||
|
|
234
|
+
if (!container.contains(element) || isRichElement(element))
|
|
259
235
|
continue;
|
|
260
236
|
const literal = document.createElement('pre');
|
|
261
237
|
literal.className = 'literal-markdown';
|
|
@@ -293,6 +269,7 @@ export class PasteController {
|
|
|
293
269
|
table.replaceWith(literal);
|
|
294
270
|
}
|
|
295
271
|
}
|
|
272
|
+
collapseClipboardWhitespace(container);
|
|
296
273
|
const imported = new Map();
|
|
297
274
|
const elements = [...container.querySelectorAll('img')];
|
|
298
275
|
const uniqueSources = new Set(elements.map((element) => element.getAttribute('src') ?? ''));
|
|
@@ -334,14 +311,18 @@ export class PasteController {
|
|
|
334
311
|
const parsed = ctx.get(parserCtx)(markdown);
|
|
335
312
|
let imageIndex = 0;
|
|
336
313
|
const hydrate = (node) => {
|
|
337
|
-
if (node
|
|
314
|
+
if (isImage(node)) {
|
|
338
315
|
const slot = /^inkkit-clipboard-image:(\d+)$/.exec(String(node.attrs.src));
|
|
339
316
|
const position = slot ? Number(slot[1]) : imageIndex++;
|
|
340
317
|
const element = elements.some((element) => element.hasAttribute('data-inkkit-image-slot'))
|
|
341
318
|
? elements.find((element) => Number(element.getAttribute('data-inkkit-authored-image-slot') ?? element.getAttribute('data-inkkit-image-slot')) === position)
|
|
342
319
|
: elements[position];
|
|
343
|
-
if (!element || !container.contains(element))
|
|
344
|
-
|
|
320
|
+
if (!element || !container.contains(element)) {
|
|
321
|
+
const text = schema.text(`[${node.attrs.alt || 'Image'}: image unavailable]`);
|
|
322
|
+
return node.isBlock
|
|
323
|
+
? schema.nodes.paragraph.create(null, text)
|
|
324
|
+
: text;
|
|
325
|
+
}
|
|
345
326
|
return node.type.create({ ...node.attrs, src: element.getAttribute('src') }, null, node.marks);
|
|
346
327
|
}
|
|
347
328
|
if (node.isText)
|
|
@@ -444,6 +425,12 @@ export class PasteController {
|
|
|
444
425
|
}
|
|
445
426
|
}
|
|
446
427
|
}
|
|
428
|
+
function insideRewrittenContent(element) {
|
|
429
|
+
for (let parent = element.parentElement; parent; parent = parent.parentElement)
|
|
430
|
+
if (!isRewrittenElement(parent))
|
|
431
|
+
return false;
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
447
434
|
function unsupportedClipboardTable(table) {
|
|
448
435
|
const rows = [...table.querySelectorAll('tr')];
|
|
449
436
|
const widths = rows.map((row) => row.querySelectorAll(':scope > th,:scope > td').length);
|
package/dist/preserve.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Node as ProseNode } from '@milkdown/kit/prose/model';
|
|
|
3
3
|
export declare class PreservationError extends Error {
|
|
4
4
|
constructor(message: string, options?: ErrorOptions);
|
|
5
5
|
}
|
|
6
|
+
export declare function correspondence(before: string[], after: string[]): Array<number | undefined>;
|
|
6
7
|
/** Retains the loaded source of blocks the edit leaves alone. */
|
|
7
8
|
export declare class Preservation {
|
|
8
9
|
private readonly ctx;
|