@aicayzer/inkkit 0.0.16 → 0.0.18
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/README.md +2 -0
- package/dist/callouts.d.ts +0 -1
- package/dist/callouts.js +12 -35
- package/dist/code-fences.js +6 -1
- package/dist/comments.d.ts +21 -0
- package/dist/comments.js +55 -4
- package/dist/core.d.ts +7 -0
- package/dist/core.js +2 -0
- package/dist/cursor.d.ts +2 -0
- package/dist/cursor.js +40 -0
- package/dist/dialect.js +109 -11
- package/dist/drop-placement.d.ts +19 -0
- package/dist/drop-placement.js +47 -0
- package/dist/editor.d.ts +32 -1
- package/dist/editor.js +293 -61
- package/dist/html-import.d.ts +13 -0
- package/dist/html-import.js +477 -0
- package/dist/image-blocks.d.ts +15 -0
- package/dist/image-blocks.js +44 -1
- package/dist/index.d.ts +1 -7
- package/dist/index.js +3 -2
- package/dist/link-safety.d.ts +17 -0
- package/dist/link-safety.js +29 -0
- package/dist/literals.d.ts +1 -0
- package/dist/literals.js +14 -9
- package/dist/mermaid.d.ts +21 -1
- package/dist/mermaid.js +92 -9
- package/dist/paste-html.d.ts +8 -0
- package/dist/paste-html.js +21 -0
- package/dist/paste.d.ts +7 -0
- package/dist/paste.js +152 -43
- package/dist/preserve.d.ts +19 -2
- package/dist/preserve.js +861 -124
- package/dist/print.js +2 -5
- package/dist/references.d.ts +2 -0
- package/dist/references.js +63 -13
- package/dist/scroll.d.ts +15 -0
- package/dist/scroll.js +27 -0
- package/dist/spreadsheet.d.ts +6 -1
- package/dist/spreadsheet.js +29 -15
- package/dist/style.css +11 -19
- package/dist/tables.js +24 -2
- package/package.json +12 -4
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { editorViewCtx, parserCtx } from '@milkdown/kit/core';
|
|
2
|
+
import { Fragment, Slice, } from '@milkdown/kit/prose/model';
|
|
3
|
+
import { EditorState } from '@milkdown/kit/prose/state';
|
|
4
|
+
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
5
|
+
import { gfmAutolinkLiteralFromMarkdown } from 'mdast-util-gfm-autolink-literal';
|
|
6
|
+
import { gfmFootnoteFromMarkdown } from 'mdast-util-gfm-footnote';
|
|
7
|
+
import { gfmStrikethroughFromMarkdown } from 'mdast-util-gfm-strikethrough';
|
|
8
|
+
import { gfmTableFromMarkdown } from 'mdast-util-gfm-table';
|
|
9
|
+
import { gfmTaskListItemFromMarkdown } from 'mdast-util-gfm-task-list-item';
|
|
10
|
+
import { gfmAutolinkLiteral } from 'micromark-extension-gfm-autolink-literal';
|
|
11
|
+
import { gfmFootnote } from 'micromark-extension-gfm-footnote';
|
|
12
|
+
import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough';
|
|
13
|
+
import { gfmTable } from 'micromark-extension-gfm-table';
|
|
14
|
+
import { gfmTaskListItem } from 'micromark-extension-gfm-task-list-item';
|
|
15
|
+
import { commentSource, commentTextEndsEarly } from './comments.js';
|
|
16
|
+
import { imageBlockName } from './image-blocks.js';
|
|
17
|
+
import { serialize } from './dialect.js';
|
|
18
|
+
import { isSafeLinkTarget, isScriptAddress } from './link-safety.js';
|
|
19
|
+
import { semanticSignature } from './preserve.js';
|
|
20
|
+
import { singleLine } from './references.js';
|
|
21
|
+
// HTML can carry values that the Markdown writer saves as they are, so content
|
|
22
|
+
// imported from HTML is kept only where its saved Markdown reads back as the
|
|
23
|
+
// same structure in InkKit, and where no reading of it holds an address that
|
|
24
|
+
// HTML import refuses. InkKit's own syntax, such as wiki links, literal source
|
|
25
|
+
// and comments, is read differently by other apps, so the saved Markdown is
|
|
26
|
+
// also read as GitHub Flavored Markdown, as strict CommonMark, and as
|
|
27
|
+
// CommonMark with raw HTML turned off.
|
|
28
|
+
const standard = () => ({
|
|
29
|
+
extensions: [
|
|
30
|
+
gfmAutolinkLiteral(),
|
|
31
|
+
gfmFootnote(),
|
|
32
|
+
gfmStrikethrough(),
|
|
33
|
+
gfmTable(),
|
|
34
|
+
gfmTaskListItem(),
|
|
35
|
+
],
|
|
36
|
+
mdastExtensions: [
|
|
37
|
+
gfmAutolinkLiteralFromMarkdown(),
|
|
38
|
+
gfmFootnoteFromMarkdown(),
|
|
39
|
+
gfmStrikethroughFromMarkdown(),
|
|
40
|
+
gfmTableFromMarkdown(),
|
|
41
|
+
gfmTaskListItemFromMarkdown(),
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
// An app that turns raw HTML off reads the Markdown inside HTML as text.
|
|
45
|
+
const withoutRawHTML = {
|
|
46
|
+
extensions: [{ disable: { null: ['htmlFlow', 'htmlText'] } }],
|
|
47
|
+
};
|
|
48
|
+
function standardTree(markdown) {
|
|
49
|
+
return fromMarkdown(markdown, standard());
|
|
50
|
+
}
|
|
51
|
+
function safeAddresses(markdown) {
|
|
52
|
+
// A scheme needs a colon, written as itself or as a character reference.
|
|
53
|
+
if (!/[:&]/.test(markdown))
|
|
54
|
+
return true;
|
|
55
|
+
const visit = (node) => {
|
|
56
|
+
if ((node.type === 'link' || node.type === 'definition') &&
|
|
57
|
+
!isSafeLinkTarget(node.url))
|
|
58
|
+
return false;
|
|
59
|
+
if (node.type === 'image' && isScriptAddress(node.url))
|
|
60
|
+
return false;
|
|
61
|
+
return !('children' in node) || node.children.every(visit);
|
|
62
|
+
};
|
|
63
|
+
return (visit(standardTree(markdown)) &&
|
|
64
|
+
visit(fromMarkdown(markdown)) &&
|
|
65
|
+
visit(fromMarkdown(markdown, withoutRawHTML)));
|
|
66
|
+
}
|
|
67
|
+
class Check {
|
|
68
|
+
ctx;
|
|
69
|
+
schema;
|
|
70
|
+
constructor(ctx, schema) {
|
|
71
|
+
this.ctx = ctx;
|
|
72
|
+
this.schema = schema;
|
|
73
|
+
}
|
|
74
|
+
base;
|
|
75
|
+
/**
|
|
76
|
+
* The document as the editor holds it once inserted, after the editor's
|
|
77
|
+
* plugins have normalised it, such as a paragraph holding only an image
|
|
78
|
+
* becoming an image block.
|
|
79
|
+
*/
|
|
80
|
+
settled(doc) {
|
|
81
|
+
this.base ??= EditorState.create({
|
|
82
|
+
schema: this.schema,
|
|
83
|
+
plugins: this.ctx.get(editorViewCtx).state.plugins,
|
|
84
|
+
});
|
|
85
|
+
const { state } = this.base.applyTransaction(this.base.tr.replaceWith(0, this.base.doc.content.size, doc.content));
|
|
86
|
+
return state === this.base ? doc : state.doc;
|
|
87
|
+
}
|
|
88
|
+
/** Whether these blocks, saved and read back, give the same structure. */
|
|
89
|
+
readsBack(blocks) {
|
|
90
|
+
return this.reading(blocks) === true;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The blocks in a form that reads back as itself: as they are, or as their
|
|
94
|
+
* Markdown reads back when the only difference is a link that the reader
|
|
95
|
+
* makes of text, such as a bare web address.
|
|
96
|
+
*/
|
|
97
|
+
kept(blocks) {
|
|
98
|
+
const reading = this.reading(blocks);
|
|
99
|
+
if (reading === true)
|
|
100
|
+
return [...blocks];
|
|
101
|
+
if (!reading || !reading.autolinks)
|
|
102
|
+
return undefined;
|
|
103
|
+
const children = reading.reread.children;
|
|
104
|
+
return this.readsBack(children) ? [...children] : undefined;
|
|
105
|
+
}
|
|
106
|
+
reading(blocks) {
|
|
107
|
+
try {
|
|
108
|
+
const doc = this.schema.topNodeType.createChecked(null, blocks);
|
|
109
|
+
const markdown = serialize(this.ctx, doc);
|
|
110
|
+
const reread = this.ctx.get(parserCtx)(markdown);
|
|
111
|
+
const read = semanticSignature(reread);
|
|
112
|
+
if (read === semanticSignature(doc) ||
|
|
113
|
+
read === this.settledSignature(doc))
|
|
114
|
+
return safeAddresses(markdown) || undefined;
|
|
115
|
+
return {
|
|
116
|
+
reread,
|
|
117
|
+
autolinks: semanticSignature(withoutAutolinks(reread)) ===
|
|
118
|
+
this.settledSignature(doc),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
settledSignature(doc) {
|
|
126
|
+
return semanticSignature(this.settled(doc));
|
|
127
|
+
}
|
|
128
|
+
/** Index of the first block that reads back differently in context. */
|
|
129
|
+
firstDifference(blocks) {
|
|
130
|
+
try {
|
|
131
|
+
const doc = this.schema.topNodeType.createChecked(null, blocks);
|
|
132
|
+
const reread = this.ctx.get(parserCtx)(serialize(this.ctx, doc));
|
|
133
|
+
const settled = this.settled(doc);
|
|
134
|
+
const signature = (node) => semanticSignature(this.schema.topNodeType.create(null, node));
|
|
135
|
+
for (let index = 0; index < settled.childCount; index++)
|
|
136
|
+
if (index >= reread.childCount ||
|
|
137
|
+
signature(settled.child(index)) !== signature(reread.child(index)))
|
|
138
|
+
return index;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
// The structure matches, so an address is unsafe only in context.
|
|
144
|
+
return blocks.findIndex((block) => !isPlain(block));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// An address that GitHub Flavored Markdown reads from text as a link.
|
|
148
|
+
function autolink(href, text) {
|
|
149
|
+
return [text, `http://${text}`, `mailto:${text}`].includes(href);
|
|
150
|
+
}
|
|
151
|
+
function withoutAutolinks(node) {
|
|
152
|
+
if (node.isText)
|
|
153
|
+
return node.mark(node.marks.filter((mark) => mark.type.name !== 'link' ||
|
|
154
|
+
!autolink(String(mark.attrs.href), node.text)));
|
|
155
|
+
if (node.isLeaf)
|
|
156
|
+
return node;
|
|
157
|
+
return node.copy(Fragment.fromArray(node.children.map(withoutAutolinks)));
|
|
158
|
+
}
|
|
159
|
+
function isPlain(node) {
|
|
160
|
+
if (node.type.name !== 'paragraph')
|
|
161
|
+
return false;
|
|
162
|
+
let plain = true;
|
|
163
|
+
node.forEach((child) => {
|
|
164
|
+
if (!child.isText || child.marks.length)
|
|
165
|
+
plain = false;
|
|
166
|
+
});
|
|
167
|
+
return plain;
|
|
168
|
+
}
|
|
169
|
+
function leafText(node) {
|
|
170
|
+
switch (node.type.name) {
|
|
171
|
+
case 'image':
|
|
172
|
+
case imageBlockName:
|
|
173
|
+
return String(node.attrs.alt ?? '');
|
|
174
|
+
case 'footnote_reference':
|
|
175
|
+
return `[${String(node.attrs.label ?? '')}]`;
|
|
176
|
+
case 'inkkit_wiki_link':
|
|
177
|
+
return String(node.attrs.label || node.attrs.target || '');
|
|
178
|
+
case 'hardbreak':
|
|
179
|
+
return '\n';
|
|
180
|
+
case 'html':
|
|
181
|
+
return String(node.attrs.value ?? '');
|
|
182
|
+
default:
|
|
183
|
+
return node.textContent;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function text(schema, value, marks) {
|
|
187
|
+
return value ? [schema.text(value, marks)] : [];
|
|
188
|
+
}
|
|
189
|
+
/** The node's text as plain paragraphs, one for each line that has text. */
|
|
190
|
+
function paragraphs(check, node) {
|
|
191
|
+
const { schema } = check;
|
|
192
|
+
const source = node.isLeaf
|
|
193
|
+
? leafText(node)
|
|
194
|
+
: node.textBetween(0, node.content.size, '\n', leafText);
|
|
195
|
+
const result = [];
|
|
196
|
+
for (const line of source.split(/\r\n?|\n/)) {
|
|
197
|
+
const value = line.trim();
|
|
198
|
+
if (!value)
|
|
199
|
+
continue;
|
|
200
|
+
const paragraph = schema.nodes.paragraph.create(null, schema.text(value));
|
|
201
|
+
result.push(...(check.kept([paragraph]) ?? []));
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
function codeBlock(schema, value) {
|
|
206
|
+
return schema.nodes.code_block.create(null, text(schema, value));
|
|
207
|
+
}
|
|
208
|
+
function literalSource(node) {
|
|
209
|
+
let source = '';
|
|
210
|
+
node.forEach((child) => {
|
|
211
|
+
source += child.isText ? child.text : commentSource(child);
|
|
212
|
+
});
|
|
213
|
+
return source;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Whether other readers take the comment as InkKit does: its text must not end
|
|
217
|
+
* it early, and GitHub Flavored Markdown, which has no `%%` comments, must
|
|
218
|
+
* read the text of one as plain text.
|
|
219
|
+
*/
|
|
220
|
+
function commentHolds(node) {
|
|
221
|
+
const body = node.textContent;
|
|
222
|
+
if (commentTextEndsEarly(String(node.attrs.syntax), body))
|
|
223
|
+
return false;
|
|
224
|
+
if (node.attrs.syntax === 'html')
|
|
225
|
+
return true;
|
|
226
|
+
const source = commentSource(node);
|
|
227
|
+
const { children } = standardTree(source);
|
|
228
|
+
const paragraph = children[0];
|
|
229
|
+
const only = paragraph?.type === 'paragraph' ? paragraph.children : [];
|
|
230
|
+
return (children.length === 1 &&
|
|
231
|
+
only.length === 1 &&
|
|
232
|
+
only[0].type === 'text' &&
|
|
233
|
+
only[0].value === source);
|
|
234
|
+
}
|
|
235
|
+
function plainComment(check, node) {
|
|
236
|
+
const { schema } = check;
|
|
237
|
+
const body = singleLine(node.textContent).trim();
|
|
238
|
+
if (node.isInline)
|
|
239
|
+
return text(schema, body);
|
|
240
|
+
return [schema.nodes.paragraph.create(null, text(schema, body))];
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Literal source and comments: InkKit shows their text, so they are kept only
|
|
244
|
+
* where every reading agrees. A literal that does not read back as itself,
|
|
245
|
+
* ending where it was written, becomes a code block with the same text. A
|
|
246
|
+
* `%%` comment whose text other readers would take as Markdown becomes an HTML
|
|
247
|
+
* comment, which they hide, and a comment that still does not hold becomes
|
|
248
|
+
* plain text.
|
|
249
|
+
*/
|
|
250
|
+
function opaque(check, node) {
|
|
251
|
+
const { schema } = check;
|
|
252
|
+
const name = node.type.name;
|
|
253
|
+
// A literal must also end where it was written, so a paragraph follows it.
|
|
254
|
+
// One that holds only whitespace holds nothing.
|
|
255
|
+
if (name === 'literal_markdown')
|
|
256
|
+
return !literalSource(node).trim()
|
|
257
|
+
? []
|
|
258
|
+
: check.readsBack([
|
|
259
|
+
node,
|
|
260
|
+
schema.nodes.paragraph.create(null, schema.text('x')),
|
|
261
|
+
])
|
|
262
|
+
? [node]
|
|
263
|
+
: [codeBlock(schema, literalSource(node))];
|
|
264
|
+
if (name === 'comment_inline' || name === 'comment_block') {
|
|
265
|
+
if (commentHolds(node))
|
|
266
|
+
return [node];
|
|
267
|
+
if (node.attrs.syntax === 'obsidian') {
|
|
268
|
+
const html = node.type.create({ ...node.attrs, syntax: 'html' }, node.content);
|
|
269
|
+
if (commentHolds(html))
|
|
270
|
+
return [html];
|
|
271
|
+
}
|
|
272
|
+
return plainComment(check, node);
|
|
273
|
+
}
|
|
274
|
+
if (node.isLeaf || node.isText)
|
|
275
|
+
return [node];
|
|
276
|
+
const children = [];
|
|
277
|
+
let changed = false;
|
|
278
|
+
node.forEach((child) => {
|
|
279
|
+
const replaced = opaque(check, child);
|
|
280
|
+
if (replaced.length !== 1 || replaced[0] !== child)
|
|
281
|
+
changed = true;
|
|
282
|
+
children.push(...replaced);
|
|
283
|
+
});
|
|
284
|
+
if (!changed)
|
|
285
|
+
return [node];
|
|
286
|
+
const content = Fragment.fromArray(children);
|
|
287
|
+
// A replacement the parent cannot hold turns the parent into text.
|
|
288
|
+
return node.type.validContent(content)
|
|
289
|
+
? [node.copy(content)]
|
|
290
|
+
: paragraphs(check, node);
|
|
291
|
+
}
|
|
292
|
+
function plainMarks(schema, marks) {
|
|
293
|
+
return marks.map((mark) => mark.type === schema.marks.link
|
|
294
|
+
? mark.type.create({ href: mark.attrs.href, title: mark.attrs.title })
|
|
295
|
+
: mark);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The block with InkKit's own metadata removed: inline nodes other than images
|
|
299
|
+
* and line breaks become their text, links lose their reference, callouts
|
|
300
|
+
* become quotes, definitions are dropped and code fences take default markers.
|
|
301
|
+
*/
|
|
302
|
+
function plainForm(check, node) {
|
|
303
|
+
const { schema } = check;
|
|
304
|
+
const name = node.type.name;
|
|
305
|
+
if (node.isText)
|
|
306
|
+
return [schema.text(node.text, plainMarks(schema, node.marks))];
|
|
307
|
+
if (name === 'comment_inline' || name === 'comment_block')
|
|
308
|
+
return plainComment(check, node);
|
|
309
|
+
if (node.isInline) {
|
|
310
|
+
if (name === 'image' || name === 'hardbreak')
|
|
311
|
+
return [node.mark(plainMarks(schema, node.marks))];
|
|
312
|
+
return text(schema, leafText(node), plainMarks(schema, node.marks));
|
|
313
|
+
}
|
|
314
|
+
if (name === 'reference_definition')
|
|
315
|
+
return [];
|
|
316
|
+
if (name === 'literal_markdown')
|
|
317
|
+
return [codeBlock(schema, literalSource(node))];
|
|
318
|
+
if (name === 'code_block')
|
|
319
|
+
return [node.type.create({ language: node.attrs.language }, node.content)];
|
|
320
|
+
const children = [];
|
|
321
|
+
node.forEach((child) => children.push(...plainForm(check, child)));
|
|
322
|
+
if (name === 'footnote_definition')
|
|
323
|
+
return children;
|
|
324
|
+
if (name === 'inkkit_callout') {
|
|
325
|
+
const title = String(node.attrs.title ?? '').trim();
|
|
326
|
+
const quote = schema.nodes.blockquote.createChecked(null, [
|
|
327
|
+
...(title
|
|
328
|
+
? [schema.nodes.paragraph.create(null, schema.text(title))]
|
|
329
|
+
: []),
|
|
330
|
+
...children,
|
|
331
|
+
]);
|
|
332
|
+
return [quote];
|
|
333
|
+
}
|
|
334
|
+
const content = Fragment.fromArray(children);
|
|
335
|
+
return node.type.validContent(content)
|
|
336
|
+
? [node.type.create(node.attrs, content, node.marks)]
|
|
337
|
+
: paragraphs(check, node);
|
|
338
|
+
}
|
|
339
|
+
function repair(check, block) {
|
|
340
|
+
const kept = check.kept([block]);
|
|
341
|
+
if (kept)
|
|
342
|
+
return kept;
|
|
343
|
+
try {
|
|
344
|
+
const plain = check.kept(plainForm(check, block));
|
|
345
|
+
if (plain)
|
|
346
|
+
return plain;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
/* A plain form that the schema refuses falls back to text. */
|
|
350
|
+
}
|
|
351
|
+
return paragraphs(check, block);
|
|
352
|
+
}
|
|
353
|
+
function blocks(check, content) {
|
|
354
|
+
const result = [];
|
|
355
|
+
content.forEach((node) => result.push(...opaque(check, node)));
|
|
356
|
+
const kept = check.kept(result);
|
|
357
|
+
if (kept)
|
|
358
|
+
return kept;
|
|
359
|
+
let repaired = result.flatMap((node) => repair(check, node));
|
|
360
|
+
// Blocks that read back alone can still merge with a neighbour. The first
|
|
361
|
+
// block that differs takes its plain form, then becomes text, then goes.
|
|
362
|
+
const simplified = new Set();
|
|
363
|
+
for (let attempt = 0, limit = repaired.length * 3 + 1; attempt < limit && !check.readsBack(repaired); attempt++) {
|
|
364
|
+
const index = Math.max(0, check.firstDifference(repaired));
|
|
365
|
+
const block = repaired[index];
|
|
366
|
+
if (!block)
|
|
367
|
+
return [];
|
|
368
|
+
let replacement;
|
|
369
|
+
if (isPlain(block))
|
|
370
|
+
replacement = [];
|
|
371
|
+
else if (simplified.has(block))
|
|
372
|
+
replacement = paragraphs(check, block);
|
|
373
|
+
else {
|
|
374
|
+
try {
|
|
375
|
+
replacement = plainForm(check, block);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
replacement = paragraphs(check, block);
|
|
379
|
+
}
|
|
380
|
+
for (const node of replacement)
|
|
381
|
+
simplified.add(node);
|
|
382
|
+
}
|
|
383
|
+
repaired = [
|
|
384
|
+
...repaired.slice(0, index),
|
|
385
|
+
...replacement,
|
|
386
|
+
...repaired.slice(index + 1),
|
|
387
|
+
];
|
|
388
|
+
}
|
|
389
|
+
return check.kept(repaired) ?? [];
|
|
390
|
+
}
|
|
391
|
+
function openDepth(content, open, start) {
|
|
392
|
+
let depth = 0;
|
|
393
|
+
let node = start ? content.firstChild : content.lastChild;
|
|
394
|
+
// Code and preserved source are never opened, so their text is not joined
|
|
395
|
+
// to the paragraph they are inserted beside.
|
|
396
|
+
while (depth < open &&
|
|
397
|
+
node &&
|
|
398
|
+
node.isBlock &&
|
|
399
|
+
!node.isLeaf &&
|
|
400
|
+
!node.type.spec.code) {
|
|
401
|
+
depth++;
|
|
402
|
+
node = start ? node.firstChild : node.lastChild;
|
|
403
|
+
}
|
|
404
|
+
return depth;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* The slice's nodes as blocks a document holds, wrapped as insertion would
|
|
408
|
+
* wrap them: inline nodes in a paragraph and list items in a list.
|
|
409
|
+
*/
|
|
410
|
+
export function topLevel(schema, content) {
|
|
411
|
+
const top = schema.topNodeType.contentMatch;
|
|
412
|
+
const result = [];
|
|
413
|
+
let run = [];
|
|
414
|
+
const close = () => {
|
|
415
|
+
const type = run[0]?.type;
|
|
416
|
+
const wrapping = type
|
|
417
|
+
? type.isInline
|
|
418
|
+
? [schema.nodes.paragraph]
|
|
419
|
+
: top.findWrapping(type)
|
|
420
|
+
: null;
|
|
421
|
+
if (wrapping?.length)
|
|
422
|
+
result.push(wrapping.reduceRight((inner, wrapper) => wrapper.create(null, inner), run));
|
|
423
|
+
else
|
|
424
|
+
result.push(...run);
|
|
425
|
+
run = [];
|
|
426
|
+
};
|
|
427
|
+
content.forEach((node) => {
|
|
428
|
+
const previous = run[0];
|
|
429
|
+
if (previous &&
|
|
430
|
+
(node.isInline ? !previous.isInline : node.type !== previous.type))
|
|
431
|
+
close();
|
|
432
|
+
if (node.isInline || !top.matchType(node.type))
|
|
433
|
+
run.push(node);
|
|
434
|
+
else {
|
|
435
|
+
close();
|
|
436
|
+
result.push(node);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
close();
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Returns the slice with every part whose Markdown would read back as
|
|
444
|
+
* different structure replaced by a form that reads back the same, keeping as
|
|
445
|
+
* much of its text and formatting as possible.
|
|
446
|
+
*/
|
|
447
|
+
export function importableSlice(ctx, slice) {
|
|
448
|
+
const first = slice.content.firstChild;
|
|
449
|
+
if (!first)
|
|
450
|
+
return slice;
|
|
451
|
+
const check = new Check(ctx, first.type.schema);
|
|
452
|
+
const inline = first.isInline;
|
|
453
|
+
const content = Fragment.fromArray(topLevel(check.schema, slice.content));
|
|
454
|
+
const result = Fragment.fromArray(blocks(check, content));
|
|
455
|
+
// Inline nodes beside blocks were wrapped, so that form is the one to
|
|
456
|
+
// insert; other wrapping, such as list items in a list, is left to insertion.
|
|
457
|
+
let inlineCount = 0;
|
|
458
|
+
slice.content.forEach((node) => {
|
|
459
|
+
if (node.isInline)
|
|
460
|
+
inlineCount++;
|
|
461
|
+
});
|
|
462
|
+
const mixed = inlineCount > 0 && inlineCount < slice.content.childCount;
|
|
463
|
+
if (result.eq(content) && !mixed) {
|
|
464
|
+
const start = openDepth(slice.content, slice.openStart, true);
|
|
465
|
+
const end = openDepth(slice.content, slice.openEnd, false);
|
|
466
|
+
return start === slice.openStart && end === slice.openEnd
|
|
467
|
+
? slice
|
|
468
|
+
: new Slice(slice.content, start, end);
|
|
469
|
+
}
|
|
470
|
+
if (inline &&
|
|
471
|
+
result.childCount === 1 &&
|
|
472
|
+
result.firstChild.type.name === 'paragraph')
|
|
473
|
+
return new Slice(result.firstChild.content, 0, 0);
|
|
474
|
+
const openStart = inline ? 1 : slice.openStart;
|
|
475
|
+
const openEnd = inline ? 1 : slice.openEnd;
|
|
476
|
+
return new Slice(result, openDepth(result, openStart, true), openDepth(result, openEnd, false));
|
|
477
|
+
}
|
package/dist/image-blocks.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MilkdownPlugin } from '@milkdown/kit/ctx';
|
|
2
2
|
import { type Node as ProseNode, type NodeType } from '@milkdown/kit/prose/model';
|
|
3
|
+
import { type EditorState } from '@milkdown/kit/prose/state';
|
|
3
4
|
import type { ParserState } from '@milkdown/kit/transformer';
|
|
4
5
|
export declare const imageBlockName = "image_block";
|
|
5
6
|
/** Attributes shared by an inline image and a standalone image block. */
|
|
@@ -60,6 +61,20 @@ export declare function markdownImageAttrs(node: MarkdownImage): {
|
|
|
60
61
|
/** Whether a node displays an authored image, inline or as its own block. */
|
|
61
62
|
export declare function isImage(node: ProseNode): boolean;
|
|
62
63
|
export declare function isImageBlock(node: ProseNode | null | undefined): boolean;
|
|
64
|
+
export type ImagePlacement = {
|
|
65
|
+
kind: 'blocks';
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'inline';
|
|
68
|
+
} | {
|
|
69
|
+
kind: 'after';
|
|
70
|
+
pos: number;
|
|
71
|
+
};
|
|
72
|
+
/** Where `insertImages` puts images for a selection. `blocks` replaces the selection with one block per
|
|
73
|
+
* image. `inline` places them on the one line of a table cell, which cannot hold blocks. `after` puts
|
|
74
|
+
* them below the nearest block that takes a following paragraph, leaving the selection and its
|
|
75
|
+
* block as written: the selection is in source that cannot hold an image, such as code, a comment
|
|
76
|
+
* or an unsupported literal block, or it spans table cells. Nothing is ever split or discarded. */
|
|
77
|
+
export declare function imagePlacement(state: EditorState): ImagePlacement;
|
|
63
78
|
/** The image a paragraph holds as its only content, without marks, which makes the paragraph a
|
|
64
79
|
* standalone image block. A linked or emphasised image stays inline so its marks are kept. */
|
|
65
80
|
export declare function standaloneImage(children: readonly ProseNode[]): ProseNode | undefined;
|
package/dist/image-blocks.js
CHANGED
|
@@ -2,6 +2,7 @@ import { keydownHandler } from '@milkdown/kit/prose/keymap';
|
|
|
2
2
|
import { GapCursor } from '@milkdown/kit/prose/gapcursor';
|
|
3
3
|
import { Fragment, } from '@milkdown/kit/prose/model';
|
|
4
4
|
import { NodeSelection, Plugin, PluginKey, Selection, TextSelection, } from '@milkdown/kit/prose/state';
|
|
5
|
+
import { CellSelection } from '@milkdown/kit/prose/tables';
|
|
5
6
|
import { $nodeSchema, $prose } from '@milkdown/kit/utils';
|
|
6
7
|
export const imageBlockName = 'image_block';
|
|
7
8
|
/** Attributes shared by an inline image and a standalone image block. */
|
|
@@ -38,6 +39,43 @@ export function isImageBlock(node) {
|
|
|
38
39
|
function accepts(parent, index, type) {
|
|
39
40
|
return parent.canReplaceWith(index, index + 1, type);
|
|
40
41
|
}
|
|
42
|
+
const cellTypes = new Set(['table_cell', 'table_header']);
|
|
43
|
+
function cellDepth($pos) {
|
|
44
|
+
for (let depth = $pos.depth; depth > 0; depth--)
|
|
45
|
+
if (cellTypes.has($pos.node(depth).type.name))
|
|
46
|
+
return depth;
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
/** Where `insertImages` puts images for a selection. `blocks` replaces the selection with one block per
|
|
50
|
+
* image. `inline` places them on the one line of a table cell, which cannot hold blocks. `after` puts
|
|
51
|
+
* them below the nearest block that takes a following paragraph, leaving the selection and its
|
|
52
|
+
* block as written: the selection is in source that cannot hold an image, such as code, a comment
|
|
53
|
+
* or an unsupported literal block, or it spans table cells. Nothing is ever split or discarded. */
|
|
54
|
+
export function imagePlacement(state) {
|
|
55
|
+
const image = state.schema.nodes.image;
|
|
56
|
+
const paragraph = state.schema.nodes.paragraph;
|
|
57
|
+
const { selection } = state;
|
|
58
|
+
const { $from, $to } = selection;
|
|
59
|
+
const unfit = selection instanceof CellSelection ||
|
|
60
|
+
[$from, $to].some(({ parent }) => parent.inlineContent &&
|
|
61
|
+
!(image && parent.type.contentMatch.matchType(image)));
|
|
62
|
+
const from = cellDepth($from);
|
|
63
|
+
const to = cellDepth($to);
|
|
64
|
+
if (!unfit && !from && !to)
|
|
65
|
+
return { kind: 'blocks' };
|
|
66
|
+
if (!unfit && from && to && $from.sameParent($to))
|
|
67
|
+
return { kind: 'inline' };
|
|
68
|
+
for (let depth = $to.depth; depth > 0; depth--) {
|
|
69
|
+
const node = $to.node(depth);
|
|
70
|
+
const parent = $to.node(depth - 1);
|
|
71
|
+
if (node.isInline)
|
|
72
|
+
continue;
|
|
73
|
+
const index = $to.index(depth - 1) + 1;
|
|
74
|
+
if (paragraph && parent.canReplaceWith(index, index, paragraph))
|
|
75
|
+
return { kind: 'after', pos: $to.after(depth) };
|
|
76
|
+
}
|
|
77
|
+
return { kind: 'after', pos: state.doc.content.size };
|
|
78
|
+
}
|
|
41
79
|
/** The image a paragraph holds as its only content, without marks, which makes the paragraph a
|
|
42
80
|
* standalone image block. A linked or emphasised image stays inline so its marks are kept. */
|
|
43
81
|
export function standaloneImage(children) {
|
|
@@ -61,10 +99,15 @@ export function readParagraph(state, node, paragraph) {
|
|
|
61
99
|
const image = standaloneImage(element.content);
|
|
62
100
|
state.close();
|
|
63
101
|
const parent = state.top();
|
|
102
|
+
const content = Fragment.fromArray(parent?.content ?? []);
|
|
103
|
+
// A parent that must open with other content, such as a list item that
|
|
104
|
+
// opens with a code fence, is filled in front of it when it closes.
|
|
105
|
+
const filler = parent?.type.contentMatch.fillBefore(content);
|
|
64
106
|
if (image &&
|
|
65
107
|
type &&
|
|
108
|
+
filler &&
|
|
66
109
|
parent?.type.contentMatch
|
|
67
|
-
.matchFragment(
|
|
110
|
+
.matchFragment(filler.append(content))
|
|
68
111
|
?.matchType(type))
|
|
69
112
|
state.addNode(type, image.attrs);
|
|
70
113
|
else {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export type { EditorEvents, EditorOptions, TextInputPreferences, CommandState, EditingMode, CaretState, Mark, Block, FormatCommand, Keymap, } from './editor.js';
|
|
3
|
-
export type { TableCommand, TableOptions } from './tables.js';
|
|
4
|
-
export type { Heading } from './outline.js';
|
|
5
|
-
export { InkKitError } from './types.js';
|
|
6
|
-
export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, WikiLinkReference, WikiLinkAdapter, FileReference, FileContext, FilePresentation, FileAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, TextRange, ReadableTextSnapshot, TextRect, ViewportInsets, ViewportOptions, ViewportSnapshot, } from './types.js';
|
|
7
|
-
export type { EditorLabels } from './labels.js';
|
|
1
|
+
export * from './core.js';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { registerMermaid } from './mermaid.js';
|
|
2
|
+
registerMermaid(async () => (await import('mermaid')).default);
|
|
3
|
+
export * from './core.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a link address may be kept. Relative paths, queries and fragments
|
|
3
|
+
* pass, and so does an address whose scheme is listed, in any letter case.
|
|
4
|
+
* Spaces, tabs and line breaks at the ends are ignored, as in a browser. Any
|
|
5
|
+
* other control character makes the address unsafe, because a browser may skip
|
|
6
|
+
* it while it reads the scheme. The scheme is the text before the first `:` in
|
|
7
|
+
* the part before the first `/`, `?`, `#` or `\`, so an unknown or malformed
|
|
8
|
+
* scheme is refused.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isSafeLinkTarget(value: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Whether an image source would run script if a renderer treated it as an
|
|
13
|
+
* address. Image sources are opaque references that a host adapter owns, so
|
|
14
|
+
* only these schemes are refused. Spaces and control characters are ignored
|
|
15
|
+
* wherever they appear, which refuses at least what a browser would run.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isScriptAddress(value: string): boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Schemes a link may keep in imported HTML and in print export. Each opens a
|
|
2
|
+
// mail, phone or web destination and runs no code.
|
|
3
|
+
const linkSchemes = new Set(['http', 'https', 'mailto', 'tel']);
|
|
4
|
+
/**
|
|
5
|
+
* Whether a link address may be kept. Relative paths, queries and fragments
|
|
6
|
+
* pass, and so does an address whose scheme is listed, in any letter case.
|
|
7
|
+
* Spaces, tabs and line breaks at the ends are ignored, as in a browser. Any
|
|
8
|
+
* other control character makes the address unsafe, because a browser may skip
|
|
9
|
+
* it while it reads the scheme. The scheme is the text before the first `:` in
|
|
10
|
+
* the part before the first `/`, `?`, `#` or `\`, so an unknown or malformed
|
|
11
|
+
* scheme is refused.
|
|
12
|
+
*/
|
|
13
|
+
export function isSafeLinkTarget(value) {
|
|
14
|
+
const target = value.replace(/^[ \t\n\f\r]+|[ \t\n\f\r]+$/g, '');
|
|
15
|
+
if (/[\u0000-\u001f\u007f]/.test(target))
|
|
16
|
+
return false;
|
|
17
|
+
const head = target.slice(0, target.search(/[/?#\\]|$/));
|
|
18
|
+
const colon = head.indexOf(':');
|
|
19
|
+
return colon < 0 || linkSchemes.has(head.slice(0, colon).toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether an image source would run script if a renderer treated it as an
|
|
23
|
+
* address. Image sources are opaque references that a host adapter owns, so
|
|
24
|
+
* only these schemes are refused. Spaces and control characters are ignored
|
|
25
|
+
* wherever they appear, which refuses at least what a browser would run.
|
|
26
|
+
*/
|
|
27
|
+
export function isScriptAddress(value) {
|
|
28
|
+
return /^(?:javascript|vbscript):/i.test(value.replace(/[\u0000-\u0020\u007f]/g, ''));
|
|
29
|
+
}
|
package/dist/literals.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ declare module 'mdast' {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
export declare function sourceFrontmatter(source: string): string | undefined;
|
|
16
|
+
export declare function lineBreakHTML(value: string): boolean;
|
|
16
17
|
export declare function createLiteralPreservation(images: boolean): import("@milkdown/kit/utils").$Remark<"preserveLiterals", unknown>;
|
|
17
18
|
export declare const preserveLiterals: import("@milkdown/kit/utils").$Remark<"preserveLiterals", unknown>;
|
|
18
19
|
export declare const literalBlock: import("@milkdown/kit/utils").$Node;
|