@rathkit/rte-core 0.1.0-alpha.4 → 0.2.0-beta.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/README.md +1 -1
- package/dist/autoformat/autoformat.js +18 -3
- package/dist/base64.js +5 -1
- package/dist/dom/index.d.ts +1 -1
- package/dist/dom/render.js +4 -4
- package/dist/dom/view.d.ts +56 -3
- package/dist/dom/view.js +174 -25
- package/dist/edit.d.ts +6 -0
- package/dist/edit.js +96 -9
- package/dist/engine.d.ts +19 -3
- package/dist/engine.js +96 -22
- package/dist/export.js +1 -1
- package/dist/html/parse.d.ts +8 -2
- package/dist/html/parse.js +6 -2
- package/dist/images/plugin.js +2 -1
- package/dist/images/view.d.ts +1 -1
- package/dist/images/view.js +4 -4
- package/dist/import.d.ts +18 -2
- package/dist/import.js +35 -27
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lists/lists.js +14 -4
- package/dist/markdown/export.js +1 -1
- package/dist/markdown/inline.d.ts +7 -0
- package/dist/markdown/inline.js +29 -20
- package/dist/markdown/parse.js +1 -1
- package/dist/markdown/plugin.js +5 -1
- package/dist/model.d.ts +5 -0
- package/dist/paste.js +9 -2
- package/dist/path.d.ts +7 -0
- package/dist/path.js +20 -7
- package/dist/plugin.d.ts +1 -1
- package/dist/policy.d.ts +41 -0
- package/dist/policy.js +40 -0
- package/dist/standard/index.d.ts +0 -1
- package/dist/standard/index.js +0 -1
- package/dist/stats.js +1 -1
- package/dist/svg/svg.js +9 -1
- package/dist/tables/cells.d.ts +1 -1
- package/dist/tables/cells.js +3 -3
- package/dist/tables/grips.js +2 -2
- package/dist/warning.d.ts +16 -0
- package/dist/warning.js +5 -0
- package/package.json +1 -1
package/dist/import.js
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import { decodeEntities } from "./html/entities.js";
|
|
2
2
|
import { parseHtml } from "./html/parse.js";
|
|
3
|
+
import { CONTENT_PROFILES, DATA_IMAGE_SIGNATURES, DEFAULT_LINK_SCHEMES, DROPPED_ELEMENTS, GROUPING_ELEMENTS, IMAGE_SCHEMES } from "./policy.js";
|
|
3
4
|
import { parseLine, SIDES, withSides } from "./cellborder.js";
|
|
4
5
|
import { COLOR_VALUE, COLUMN_WIDTH, MAX_INDENT, NOTE_ID, ROW_HEIGHT, inlineText, normalizeInline, } from "./model.js";
|
|
5
6
|
import { asChildren } from "./path.js";
|
|
6
7
|
import { DEFAULT_MARKER } from "./model.js";
|
|
7
|
-
const DROP =
|
|
8
|
-
|
|
9
|
-
"head", "textarea", "input", "select", "option", "button", "canvas", "audio", "video", "applet", "xml",
|
|
10
|
-
]);
|
|
11
|
-
const CONTAINERS = new Set([
|
|
12
|
-
"div", "section", "article", "header", "footer", "aside", "nav", "figure", "figcaption", "address", "main", "form",
|
|
13
|
-
"body", "html", "center", "details", "summary", "dd", "dt", "fieldset", "hgroup",
|
|
14
|
-
]);
|
|
8
|
+
const DROP = DROPPED_ELEMENTS;
|
|
9
|
+
const CONTAINERS = GROUPING_ELEMENTS;
|
|
15
10
|
const MARK_TAGS = {
|
|
16
11
|
strong: "bold", b: "bold", em: "italic", i: "italic", cite: "italic", dfn: "italic", var: "italic", u: "underline", ins: "underline",
|
|
17
12
|
s: "strike", strike: "strike", del: "strike", code: "code", tt: "code", kbd: "code", samp: "code", sup: "sup", sub: "sub",
|
|
18
13
|
};
|
|
19
|
-
const RASTER = [
|
|
20
|
-
["image/png", "iVBORw0KGgo"], ["image/jpeg", "/9j/"], ["image/gif", "R0lGOD"], ["image/webp", "UklGR"], ["image/bmp", "Qk"],
|
|
21
|
-
];
|
|
22
14
|
const COLOR = COLOR_VALUE;
|
|
23
15
|
export function parseStyle(style) {
|
|
24
16
|
const out = new Map();
|
|
@@ -78,9 +70,11 @@ function safeImageSrc(raw, settings) {
|
|
|
78
70
|
if (!match)
|
|
79
71
|
return undefined;
|
|
80
72
|
const body = match[2];
|
|
81
|
-
return
|
|
73
|
+
return DATA_IMAGE_SIGNATURES.some(([, magic]) => body.startsWith(magic)) ? value : undefined;
|
|
82
74
|
}
|
|
83
|
-
|
|
75
|
+
if (!settings.relative && !/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(value.replace(INVISIBLE, "")))
|
|
76
|
+
return undefined;
|
|
77
|
+
return safeUrl(value, new Set(IMAGE_SCHEMES));
|
|
84
78
|
}
|
|
85
79
|
class Sink {
|
|
86
80
|
blocks = [];
|
|
@@ -508,7 +502,7 @@ function convertList(el, ctx, settings, notes = false) {
|
|
|
508
502
|
continue;
|
|
509
503
|
if (child.tag === "ul" || child.tag === "ol") {
|
|
510
504
|
const nested = convertList(child, ctx, settings);
|
|
511
|
-
const last = list.items[list.items.length - 1] ?? list.items[list.items.push({ content: [], children: [] }) - 1];
|
|
505
|
+
const last = list.items[list.items.length - 1] ?? list.items[list.items.push({ content: [], children: [], bare: true }) - 1];
|
|
512
506
|
last.children.push(nested);
|
|
513
507
|
continue;
|
|
514
508
|
}
|
|
@@ -548,6 +542,8 @@ function convertList(el, ctx, settings, notes = false) {
|
|
|
548
542
|
if (id !== undefined)
|
|
549
543
|
item.id = id;
|
|
550
544
|
}
|
|
545
|
+
if (item.content.length === 0 && !task && item.children.length > 0 && /list-style(-type)?\s*:\s*none/i.test(child.attrs.style ?? ""))
|
|
546
|
+
item.bare = true;
|
|
551
547
|
const level = levelOf(child.attrs["data-aria-level"] ?? child.attrs["aria-level"]);
|
|
552
548
|
if (level !== undefined)
|
|
553
549
|
item.level = level;
|
|
@@ -675,7 +671,7 @@ function rowHeights(rowEls, kept) {
|
|
|
675
671
|
return heights.some((h) => h !== null) ? heights : undefined;
|
|
676
672
|
}
|
|
677
673
|
function columnWidths(table, rows) {
|
|
678
|
-
const cols =
|
|
674
|
+
const cols = rows.reduce((most, row) => Math.max(most, row.reduce((sum, cell) => sum + cell.colspan, 0)), 0);
|
|
679
675
|
const widths = [];
|
|
680
676
|
const visit = (nodes) => {
|
|
681
677
|
for (const n of nodes) {
|
|
@@ -696,7 +692,7 @@ function columnWidths(table, rows) {
|
|
|
696
692
|
if (firstRow)
|
|
697
693
|
for (const cell of firstRow.children)
|
|
698
694
|
if (cell.type === "el" && (cell.tag === "td" || cell.tag === "th")) {
|
|
699
|
-
const span = Math.max(Number.parseInt(cell.attrs.colspan ?? "1", 10) || 1, 1);
|
|
695
|
+
const span = Math.min(Math.max(Number.parseInt(cell.attrs.colspan ?? "1", 10) || 1, 1), 100);
|
|
700
696
|
if (span === 1)
|
|
701
697
|
widths.push(widthOf(cell));
|
|
702
698
|
else
|
|
@@ -731,7 +727,7 @@ function padRows(rows) {
|
|
|
731
727
|
reach[k] = reach[k] - 1;
|
|
732
728
|
return used;
|
|
733
729
|
});
|
|
734
|
-
const widest = Math.max(
|
|
730
|
+
const widest = widths.reduce((most, w) => Math.max(most, w), 0);
|
|
735
731
|
return rows.map((cells, i) => {
|
|
736
732
|
const missing = widest - widths[i];
|
|
737
733
|
return missing > 0 ? [...cells, ...Array.from({ length: missing }, () => ({ header: false, colspan: 1, rowspan: 1, blocks: [{ type: "p", content: [] }] }))] : cells;
|
|
@@ -781,7 +777,7 @@ function nestByLevel(lists) {
|
|
|
781
777
|
const fresh = { type: list.type, items: [] };
|
|
782
778
|
const parent = stack[stack.length - 1];
|
|
783
779
|
if (parent) {
|
|
784
|
-
const host = parent.items[parent.items.length - 1] ?? parent.items[parent.items.push({ content: [], children: [] }) - 1];
|
|
780
|
+
const host = parent.items[parent.items.length - 1] ?? parent.items[parent.items.push({ content: [], children: [], bare: true }) - 1];
|
|
785
781
|
host.children.push(fresh);
|
|
786
782
|
}
|
|
787
783
|
else {
|
|
@@ -800,7 +796,7 @@ function nestByLevel(lists) {
|
|
|
800
796
|
return result.map((l) => freezeList(l, 0));
|
|
801
797
|
}
|
|
802
798
|
function freezeList(list, depth) {
|
|
803
|
-
const items = list.items.map((item) => ({ content: item.content, children: item.children.map((child) => (child.type === "ul" || child.type === "ol" ? freezeList(child, depth + 1) : child)), ...(item.checked !== undefined ? { checked: item.checked } : {}), ...(item.id !== undefined ? { id: item.id } : {}) }));
|
|
799
|
+
const items = list.items.map((item) => ({ content: item.content, children: item.children.map((child) => (child.type === "ul" || child.type === "ol" ? freezeList(child, depth + 1) : child)), ...(item.checked !== undefined ? { checked: item.checked } : {}), ...(item.id !== undefined ? { id: item.id } : {}), ...(item.bare ? { bare: true } : {}) }));
|
|
804
800
|
const style = list.style !== undefined && list.style !== DEFAULT_MARKER(list.type, depth) ? list.style : undefined;
|
|
805
801
|
return { type: list.type, ...(list.notes ? { notes: true } : {}), ...(list.start !== undefined ? { start: list.start } : {}), ...(style !== undefined ? { style } : {}), items };
|
|
806
802
|
}
|
|
@@ -821,17 +817,29 @@ function repairRewrittenSpaces(nodes) {
|
|
|
821
817
|
export function importBlocks(html, options = {}) {
|
|
822
818
|
const settings = {
|
|
823
819
|
allowStyles: (options.styles ?? "allowlist") === "allowlist",
|
|
824
|
-
schemes: new Set(options.linkSchemes ??
|
|
820
|
+
schemes: new Set(options.linkSchemes ?? DEFAULT_LINK_SCHEMES),
|
|
825
821
|
keepEmpty: options.keepEmptyBlocks ?? false,
|
|
826
|
-
maxData: options.maxDataImageLength ??
|
|
822
|
+
maxData: options.maxDataImageLength ?? CONTENT_PROFILES.standard.maxDataImageLength,
|
|
823
|
+
relative: options.relativeImages ?? true,
|
|
827
824
|
svg: options.sanitizeSvg,
|
|
828
825
|
recognizers: options.recognizers ?? [],
|
|
829
826
|
};
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
827
|
+
const maxInput = options.maxInputLength ?? CONTENT_PROFILES.standard.maxInputLength;
|
|
828
|
+
if (html.length > maxInput) {
|
|
829
|
+
options.onLimit?.({ limit: "maxInputLength", max: maxInput, length: html.length });
|
|
830
|
+
return [];
|
|
831
|
+
}
|
|
832
|
+
try {
|
|
833
|
+
const sink = new Sink(settings);
|
|
834
|
+
const tree = parseHtml(html, options.maxDepth ?? CONTENT_PROFILES.standard.maxDepth);
|
|
835
|
+
convertNodes(/-webkit-text-stroke-width/.test(html) ? repairRewrittenSpaces(tree) : tree, sink, { marks: [], inLink: false, pre: false }, settings);
|
|
836
|
+
sink.flush();
|
|
837
|
+
return finishBlocks(sink.blocks);
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
options.onError?.(error);
|
|
841
|
+
return [];
|
|
842
|
+
}
|
|
835
843
|
}
|
|
836
844
|
export function importHtml(html, options = {}) {
|
|
837
845
|
const blocks = importBlocks(html, { keepEmptyBlocks: true, ...options });
|
|
@@ -854,4 +862,4 @@ export function importText(value) {
|
|
|
854
862
|
}
|
|
855
863
|
const decodeLineBreaks = (value) => value.replace(/\r\n?|\u2028|\u2029/g, "\n").replace(/^\n+|\n+$/g, "");
|
|
856
864
|
export { decodeEntities };
|
|
857
|
-
export const checkImageSrc = (src, maxDataLength = 12_000_000, sanitizeSvg) => safeImageSrc(src, { maxData: maxDataLength, svg: sanitizeSvg });
|
|
865
|
+
export const checkImageSrc = (src, maxDataLength = 12_000_000, sanitizeSvg, relative = true) => safeImageSrc(src, { maxData: maxDataLength, svg: sanitizeSvg, relative });
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export declare const PACKAGE_NAME: "@rathkit/rte-core";
|
|
2
1
|
export * from "./model.ts";
|
|
3
2
|
export { checkImageSrc, importBlocks, importHtml, importText, safeUrl, parseStyle, type ImportOptions } from "./import.ts";
|
|
3
|
+
export { WARNING_CODES, type Warning, type WarningCode } from "./warning.ts";
|
|
4
|
+
export { CONTENT_PROFILES, contentPolicy, type ContentLimits, type ContentOptions, type ContentPolicy, type ContentProfile } from "./policy.ts";
|
|
4
5
|
export { exportBlock, exportHtml, exportInline } from "./export.ts";
|
|
5
6
|
export * from "./path.ts";
|
|
6
7
|
export * from "./edit.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export const PACKAGE_NAME = "@rathkit/rte-core";
|
|
2
1
|
export * from "./model.js";
|
|
3
2
|
export { checkImageSrc, importBlocks, importHtml, importText, safeUrl, parseStyle } from "./import.js";
|
|
3
|
+
export { WARNING_CODES } from "./warning.js";
|
|
4
|
+
export { CONTENT_PROFILES, contentPolicy } from "./policy.js";
|
|
4
5
|
export { exportBlock, exportHtml, exportInline } from "./export.js";
|
|
5
6
|
export * from "./path.js";
|
|
6
7
|
export * from "./edit.js";
|
package/dist/lists/lists.js
CHANGED
|
@@ -68,7 +68,7 @@ function splitList(list, path, liftChildren) {
|
|
|
68
68
|
const item = list.items[i];
|
|
69
69
|
const numbering = (count) => (list.type === "ol" ? { start: (list.start ?? 1) + count } : {});
|
|
70
70
|
if (k === undefined) {
|
|
71
|
-
const placeholder = item.children.length > 0 && !liftChildren ? [{ content: [], children: item.children }] : [];
|
|
71
|
+
const placeholder = item.children.length > 0 && !liftChildren ? [{ content: [], children: item.children, bare: true }] : [];
|
|
72
72
|
const rest = [...placeholder, ...list.items.slice(i + 1)];
|
|
73
73
|
return {
|
|
74
74
|
before: i > 0 ? { ...list, items: list.items.slice(0, i) } : undefined,
|
|
@@ -80,7 +80,7 @@ function splitList(list, path, liftChildren) {
|
|
|
80
80
|
const inner = splitList(item.children[k], path.slice(2), liftChildren);
|
|
81
81
|
const keptSublists = [...item.children.slice(0, k), ...(inner.before ? [inner.before] : [])];
|
|
82
82
|
const afterSublists = [...(inner.after ? [inner.after] : []), ...item.children.slice(k + 1)];
|
|
83
|
-
const rest = [...(afterSublists.length > 0 ? [{ content: [], children: afterSublists }] : []), ...list.items.slice(i + 1)];
|
|
83
|
+
const rest = [...(afterSublists.length > 0 ? [{ content: [], children: afterSublists, bare: true }] : []), ...list.items.slice(i + 1)];
|
|
84
84
|
return {
|
|
85
85
|
before: { ...list, items: [...list.items.slice(0, i), { content: item.content, children: keptSublists }] },
|
|
86
86
|
content: inner.content,
|
|
@@ -271,7 +271,7 @@ export function indentItems(doc, sel, maxDepth = Number.POSITIVE_INFINITY) {
|
|
|
271
271
|
const rest = list.items.slice(index + group.length);
|
|
272
272
|
if (previous)
|
|
273
273
|
return { ...list, items: [...list.items.slice(0, index - 1), nested(previous), ...rest] };
|
|
274
|
-
return { ...list, items: [nested({ content: [], children: [] }), ...rest] };
|
|
274
|
+
return { ...list, items: [nested({ content: [], children: [], bare: true }), ...rest] };
|
|
275
275
|
});
|
|
276
276
|
}
|
|
277
277
|
return { doc: next, selection: carry(doc, next, sel) };
|
|
@@ -286,6 +286,10 @@ export function outdentItems(doc, sel) {
|
|
|
286
286
|
if (!loc)
|
|
287
287
|
continue;
|
|
288
288
|
if (loc.item.length === 1) {
|
|
289
|
+
const here = listOfItem(next, path);
|
|
290
|
+
const held = here?.list.items[here.index];
|
|
291
|
+
if (held && held.content.length === 0 && held.children.length > 0)
|
|
292
|
+
continue;
|
|
289
293
|
next = takeOutOfList(next, path, true);
|
|
290
294
|
continue;
|
|
291
295
|
}
|
|
@@ -300,6 +304,7 @@ export function outdentItems(doc, sel) {
|
|
|
300
304
|
const keptSub = itemIndex > 0 ? [{ ...sub, items: sub.items.slice(0, itemIndex) }] : [];
|
|
301
305
|
const lifted = {
|
|
302
306
|
content: moved.content,
|
|
307
|
+
...(moved.bare ? { bare: true } : {}),
|
|
303
308
|
children: [...moved.children, ...(followers.length > 0 ? [{ type: sub.type, ...(sub.style !== undefined ? { style: sub.style } : {}), items: followers }] : [])],
|
|
304
309
|
};
|
|
305
310
|
const children = [...parent.children.slice(0, sublistIndex), ...keptSub, ...parent.children.slice(sublistIndex + 1)];
|
|
@@ -355,6 +360,11 @@ export function backspaceAtItemStart(doc, pos) {
|
|
|
355
360
|
const held = listOfItem(doc, pos.path);
|
|
356
361
|
if (held?.list.items[held.index]?.checked !== undefined)
|
|
357
362
|
return { doc: setChecked(doc, pos.path, undefined), selection: caret(pos) };
|
|
363
|
+
const only = held?.list.items[held.index];
|
|
364
|
+
if (only && only.content.length === 0 && only.bare !== true && (only.children[0]?.type === "ul" || only.children[0]?.type === "ol")) {
|
|
365
|
+
const next = mapListAt(doc, pos.path, (list, index) => ({ ...list, items: list.items.map((item, i) => (i === index ? { ...item, bare: true } : item)) }));
|
|
366
|
+
return { doc: next, selection: caret(pos) };
|
|
367
|
+
}
|
|
358
368
|
if (!held || held.index !== 0)
|
|
359
369
|
return undefined;
|
|
360
370
|
return leaveListItem(doc, pos);
|
|
@@ -375,7 +385,7 @@ export function splitListItem(doc, pos) {
|
|
|
375
385
|
...list,
|
|
376
386
|
items: list.items.flatMap((candidate, i) => (i !== index ? [candidate] : [
|
|
377
387
|
{ content: before, children: [], ...(item.checked !== undefined ? { checked: item.checked } : {}), ...(item.id !== undefined ? { id: item.id } : {}) },
|
|
378
|
-
{ content: after, children: item.children, ...(item.checked !== undefined ? { checked: false } : {}) },
|
|
388
|
+
{ content: after, children: item.children, ...(item.checked !== undefined ? { checked: false } : {}), ...(item.bare && after.length === 0 ? { bare: true } : {}) },
|
|
379
389
|
])),
|
|
380
390
|
}));
|
|
381
391
|
return { doc: next, selection: caret({ path: containerPaths(next)[ordinal + 1] ?? pos.path, offset: 0 }) };
|
package/dist/markdown/export.js
CHANGED
|
@@ -66,7 +66,7 @@ function inline(content, flat, table) {
|
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
const lead = /^\s*/.exec(text)?.[0] ?? "";
|
|
69
|
-
const trail = lead.length === text.length ? "" :
|
|
69
|
+
const trail = lead.length === text.length ? "" : text.slice(text.trimEnd().length);
|
|
70
70
|
const core = text.slice(lead.length, text.length - trail.length);
|
|
71
71
|
const prev = runs[r - 1];
|
|
72
72
|
const next = runs[r + 1];
|
|
@@ -6,6 +6,13 @@ export interface MarkdownOptions {
|
|
|
6
6
|
readonly sanitizeSvg?: ((dataUrl: string) => string | undefined) | undefined;
|
|
7
7
|
/** Largest `data:` image address, in characters. */
|
|
8
8
|
readonly maxDataImageLength?: number;
|
|
9
|
+
/** Keep an image whose address is relative. Default true. */
|
|
10
|
+
readonly relativeImages?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* How deep lists and quotes nest before what is inside stays text, at most 24 whatever is given (the editor passes its content profile's
|
|
13
|
+
* `maxDepth`, so a stricter profile reads less). Each level reads the lines inside it again, so this also bounds the work to 24 times the text.
|
|
14
|
+
*/
|
|
15
|
+
readonly maxDepth?: number;
|
|
9
16
|
}
|
|
10
17
|
/** Link reference definitions found in the document: `[label]: address "title"`. Keys come from `refLabel`. */
|
|
11
18
|
export type Refs = ReadonlyMap<string, {
|
package/dist/markdown/inline.js
CHANGED
|
@@ -39,10 +39,21 @@ export function parseInline(src, options = {}) {
|
|
|
39
39
|
let head = null;
|
|
40
40
|
let tail = null;
|
|
41
41
|
let buffer = "";
|
|
42
|
+
let pending = "";
|
|
43
|
+
const add = (text) => {
|
|
44
|
+
const kept = text.trimEnd().length;
|
|
45
|
+
if (kept === 0) {
|
|
46
|
+
pending += text;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
buffer += pending + text.slice(0, kept);
|
|
50
|
+
pending = text.slice(kept);
|
|
51
|
+
};
|
|
42
52
|
const flush = () => {
|
|
43
|
-
if (buffer !== "") {
|
|
44
|
-
pieces.push({ k: "t", s: buffer });
|
|
53
|
+
if (buffer !== "" || pending !== "") {
|
|
54
|
+
pieces.push({ k: "t", s: buffer + pending });
|
|
45
55
|
buffer = "";
|
|
56
|
+
pending = "";
|
|
46
57
|
}
|
|
47
58
|
};
|
|
48
59
|
const unlink = (d) => {
|
|
@@ -187,25 +198,23 @@ export function parseInline(src, options = {}) {
|
|
|
187
198
|
continue;
|
|
188
199
|
}
|
|
189
200
|
if (isAsciiPunct(n)) {
|
|
190
|
-
|
|
201
|
+
add(n);
|
|
191
202
|
i += 2;
|
|
192
203
|
continue;
|
|
193
204
|
}
|
|
194
|
-
|
|
205
|
+
add(c);
|
|
195
206
|
i++;
|
|
196
207
|
continue;
|
|
197
208
|
}
|
|
198
209
|
if (c === "\n") {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
spaces = buffer.length - kept;
|
|
202
|
-
buffer = buffer.slice(0, kept);
|
|
210
|
+
const spaces = pending.length;
|
|
211
|
+
pending = "";
|
|
203
212
|
if (spaces >= 2) {
|
|
204
213
|
flush();
|
|
205
214
|
pieces.push({ k: "br" });
|
|
206
215
|
}
|
|
207
216
|
else
|
|
208
|
-
|
|
217
|
+
add(" ");
|
|
209
218
|
i++;
|
|
210
219
|
while (src[i] === " " || src[i] === "\t")
|
|
211
220
|
i++;
|
|
@@ -214,11 +223,11 @@ export function parseInline(src, options = {}) {
|
|
|
214
223
|
if (c === "&") {
|
|
215
224
|
const m = /^&(?:#\d{1,7}|#[xX][0-9a-fA-F]{1,6}|[A-Za-z][A-Za-z0-9]{1,31});/.exec(src.slice(i, i + 40));
|
|
216
225
|
if (m) {
|
|
217
|
-
|
|
226
|
+
add(decodeEntities(m[0]));
|
|
218
227
|
i += m[0].length;
|
|
219
228
|
continue;
|
|
220
229
|
}
|
|
221
|
-
|
|
230
|
+
add(c);
|
|
222
231
|
i++;
|
|
223
232
|
continue;
|
|
224
233
|
}
|
|
@@ -228,7 +237,7 @@ export function parseInline(src, options = {}) {
|
|
|
228
237
|
run++;
|
|
229
238
|
const end = closingTicks(src, i + run, run);
|
|
230
239
|
if (end === -1) {
|
|
231
|
-
|
|
240
|
+
add("`".repeat(run));
|
|
232
241
|
i += run;
|
|
233
242
|
continue;
|
|
234
243
|
}
|
|
@@ -245,7 +254,7 @@ export function parseInline(src, options = {}) {
|
|
|
245
254
|
while (src[i + run] === c)
|
|
246
255
|
run++;
|
|
247
256
|
if (c === "~" && run > 2) {
|
|
248
|
-
|
|
257
|
+
add(c.repeat(run));
|
|
249
258
|
i += run;
|
|
250
259
|
continue;
|
|
251
260
|
}
|
|
@@ -270,7 +279,7 @@ export function parseInline(src, options = {}) {
|
|
|
270
279
|
const image = c === "!";
|
|
271
280
|
flush();
|
|
272
281
|
if (brackets.length >= MAX_BRACKETS) {
|
|
273
|
-
|
|
282
|
+
add(image ? "![" : "[");
|
|
274
283
|
i += image ? 2 : 1;
|
|
275
284
|
continue;
|
|
276
285
|
}
|
|
@@ -283,14 +292,14 @@ export function parseInline(src, options = {}) {
|
|
|
283
292
|
if (c === "]") {
|
|
284
293
|
const top = brackets.pop();
|
|
285
294
|
if (!top) {
|
|
286
|
-
|
|
295
|
+
add(c);
|
|
287
296
|
i++;
|
|
288
297
|
continue;
|
|
289
298
|
}
|
|
290
299
|
if (!top.active) {
|
|
291
300
|
flush();
|
|
292
301
|
asText(top);
|
|
293
|
-
|
|
302
|
+
add(c);
|
|
294
303
|
i++;
|
|
295
304
|
continue;
|
|
296
305
|
}
|
|
@@ -311,7 +320,7 @@ export function parseInline(src, options = {}) {
|
|
|
311
320
|
if (!tailInfo) {
|
|
312
321
|
flush();
|
|
313
322
|
asText(top);
|
|
314
|
-
|
|
323
|
+
add(c);
|
|
315
324
|
i++;
|
|
316
325
|
continue;
|
|
317
326
|
}
|
|
@@ -331,7 +340,7 @@ export function parseInline(src, options = {}) {
|
|
|
331
340
|
}
|
|
332
341
|
p.dead = true;
|
|
333
342
|
}
|
|
334
|
-
const safe = checkImageSrc(decodeEntities(tailInfo.dest), options.maxDataImageLength, options.sanitizeSvg);
|
|
343
|
+
const safe = checkImageSrc(decodeEntities(tailInfo.dest), options.maxDataImageLength, options.sanitizeSvg, options.relativeImages ?? true);
|
|
335
344
|
const img = top;
|
|
336
345
|
if (safe) {
|
|
337
346
|
Object.assign(img, { k: "img", src: safe, alt });
|
|
@@ -368,7 +377,7 @@ export function parseInline(src, options = {}) {
|
|
|
368
377
|
i = tag.end;
|
|
369
378
|
continue;
|
|
370
379
|
}
|
|
371
|
-
|
|
380
|
+
add(c);
|
|
372
381
|
i++;
|
|
373
382
|
continue;
|
|
374
383
|
}
|
|
@@ -381,7 +390,7 @@ export function parseInline(src, options = {}) {
|
|
|
381
390
|
continue;
|
|
382
391
|
}
|
|
383
392
|
}
|
|
384
|
-
|
|
393
|
+
add(c);
|
|
385
394
|
i++;
|
|
386
395
|
}
|
|
387
396
|
flush();
|
package/dist/markdown/parse.js
CHANGED
|
@@ -394,7 +394,7 @@ function parseList(lines, start, depth, options) {
|
|
|
394
394
|
}
|
|
395
395
|
function parseBlocks(lines, depth, options) {
|
|
396
396
|
const out = [];
|
|
397
|
-
if (depth >= MAX_DEPTH) {
|
|
397
|
+
if (depth >= Math.min(MAX_DEPTH, options.maxDepth ?? MAX_DEPTH)) {
|
|
398
398
|
const p = paragraph(lines.join("\n"), options);
|
|
399
399
|
return p ? [p] : [];
|
|
400
400
|
}
|
package/dist/markdown/plugin.js
CHANGED
|
@@ -23,7 +23,7 @@ export function markdown(options = {}) {
|
|
|
23
23
|
return false;
|
|
24
24
|
const { doc, selection } = ctx.snapshot;
|
|
25
25
|
const blocks = host.ops.isCollapsed(selection) ? doc.blocks : host.ops.extractSelection(doc, selection);
|
|
26
|
-
clipboard.writeText(exportMarkdown({ blocks }, { inlineHtml: options.inlineHtml === true })).catch((error) => host.editor.
|
|
26
|
+
clipboard.writeText(exportMarkdown({ blocks }, { inlineHtml: options.inlineHtml === true })).catch((error) => host.editor.report({ code: "copy-failed", plugin: "rathkit.markdown", message: `Copy as Markdown failed: ${error instanceof Error ? error.message : String(error)}`, error }));
|
|
27
27
|
return true;
|
|
28
28
|
},
|
|
29
29
|
});
|
|
@@ -39,12 +39,16 @@ export function markdown(options = {}) {
|
|
|
39
39
|
const path = selection.focus.path;
|
|
40
40
|
if (kindAt(doc, path) === "pre" || getBlock(doc, path)?.type === "pre")
|
|
41
41
|
return undefined;
|
|
42
|
+
if (text.length > (host.editor.importOptions.maxInputLength ?? Infinity))
|
|
43
|
+
return undefined;
|
|
42
44
|
if (mode === "detect" && !looksLikeMarkdown(text))
|
|
43
45
|
return undefined;
|
|
44
46
|
const blocks = parseMarkdown(text, {
|
|
45
47
|
...(options.linkSchemes ? { linkSchemes: options.linkSchemes } : {}),
|
|
46
48
|
...(options.maxDataImageLength !== undefined ? { maxDataImageLength: options.maxDataImageLength } : {}),
|
|
47
49
|
sanitizeSvg: host.editor.importOptions.sanitizeSvg,
|
|
50
|
+
relativeImages: host.editor.importOptions.pasteRelativeImages ?? false,
|
|
51
|
+
...(host.editor.importOptions.maxDepth !== undefined ? { maxDepth: host.editor.importOptions.maxDepth } : {}),
|
|
48
52
|
});
|
|
49
53
|
return blocks.length > 0 ? blocks : undefined;
|
|
50
54
|
});
|
package/dist/model.d.ts
CHANGED
|
@@ -76,6 +76,11 @@ export interface ListItem {
|
|
|
76
76
|
readonly checked?: boolean;
|
|
77
77
|
/** In a notes list: the id of the reference this note belongs to. */
|
|
78
78
|
readonly id?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Set on an item that has no text and holds only a nested list, and shows no marker of its own: the nested list sits under it
|
|
81
|
+
* without a bullet or number above it. Without this an item with no text still shows its marker. Ignored once the item has text.
|
|
82
|
+
*/
|
|
83
|
+
readonly bare?: true;
|
|
79
84
|
}
|
|
80
85
|
/** What a list item may hold besides its own text. Tables and definition lists are not allowed in an item. */
|
|
81
86
|
export type ItemChild = List | Paragraph | Heading | CodeBlock | Quote | Rule;
|
package/dist/paste.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { importBlocks, importText } from "./import.js";
|
|
2
|
+
import { CONTENT_PROFILES } from "./policy.js";
|
|
2
3
|
const START = "<!--StartFragment-->";
|
|
3
4
|
const END = "<!--EndFragment-->";
|
|
4
5
|
export function extractFragment(html) {
|
|
@@ -17,11 +18,17 @@ function unwrapSingleCell(blocks) {
|
|
|
17
18
|
}
|
|
18
19
|
export function defaultPaste(data, importOptions = {}) {
|
|
19
20
|
if (data.html && data.html.trim() !== "") {
|
|
20
|
-
const blocks = importBlocks(data.html, { ...importOptions, styles: "semantic" });
|
|
21
|
+
const blocks = importBlocks(data.html, { ...importOptions, styles: "semantic", relativeImages: importOptions.pasteRelativeImages ?? false });
|
|
21
22
|
if (blocks.length > 0)
|
|
22
23
|
return unwrapSingleCell(blocks);
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
const text = data.text ?? "";
|
|
26
|
+
const max = importOptions.maxInputLength ?? CONTENT_PROFILES.standard.maxInputLength;
|
|
27
|
+
if (text.length > max) {
|
|
28
|
+
importOptions.onLimit?.({ limit: "maxInputLength", max, length: text.length });
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return importText(text);
|
|
25
32
|
}
|
|
26
33
|
export function pasteBlocks(data, handlers = [], importOptions = {}) {
|
|
27
34
|
for (const handler of handlers) {
|
package/dist/path.d.ts
CHANGED
|
@@ -21,11 +21,18 @@ export declare const pathKey: (path: Path) => string;
|
|
|
21
21
|
* nested under it, rows run left to right then top to bottom, and so on. Negative when `a` comes first.
|
|
22
22
|
*/
|
|
23
23
|
export declare function comparePath(a: Path, b: Path): number;
|
|
24
|
+
/** A horizontal rule: where it is, and how many containers come before it in reading order. */
|
|
25
|
+
export interface RuleAt {
|
|
26
|
+
readonly path: Path;
|
|
27
|
+
readonly before: number;
|
|
28
|
+
}
|
|
24
29
|
/**
|
|
25
30
|
* Every container in the document, in reading order. Documents are never changed once made, so the answer is remembered
|
|
26
31
|
* per document. The array is shared: do not change it.
|
|
27
32
|
*/
|
|
28
33
|
export declare function containerPaths(doc: Doc): Path[];
|
|
34
|
+
/** Every horizontal rule, in reading order, with the number of containers before it. A rule between container n-1 and n has `before` n. The array is shared. */
|
|
35
|
+
export declare function rulePaths(doc: Doc): RuleAt[];
|
|
29
36
|
export declare function blockContainers(block: Block, prefix: readonly number[]): Path[];
|
|
30
37
|
/** True when the path leads to a container that exists. Cheap: it follows the path instead of listing every container. */
|
|
31
38
|
export declare function hasContainer(doc: Doc, path: Path): boolean;
|
package/dist/path.js
CHANGED
|
@@ -11,13 +11,13 @@ export function comparePath(a, b) {
|
|
|
11
11
|
}
|
|
12
12
|
return a.length - b.length;
|
|
13
13
|
}
|
|
14
|
-
function listPaths(list, prefix, out) {
|
|
14
|
+
function listPaths(list, prefix, out, rules) {
|
|
15
15
|
list.items.forEach((item, i) => {
|
|
16
16
|
out.push([...prefix, i]);
|
|
17
|
-
item.children.forEach((child, k) => blockPaths(child, [...prefix, i, k], out));
|
|
17
|
+
item.children.forEach((child, k) => blockPaths(child, [...prefix, i, k], out, rules));
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
-
function blockPaths(block, prefix, out) {
|
|
20
|
+
function blockPaths(block, prefix, out, rules) {
|
|
21
21
|
switch (block.type) {
|
|
22
22
|
case "p":
|
|
23
23
|
case "h":
|
|
@@ -26,18 +26,20 @@ function blockPaths(block, prefix, out) {
|
|
|
26
26
|
break;
|
|
27
27
|
case "ul":
|
|
28
28
|
case "ol":
|
|
29
|
-
listPaths(block, prefix, out);
|
|
29
|
+
listPaths(block, prefix, out, rules);
|
|
30
30
|
break;
|
|
31
31
|
case "dl":
|
|
32
32
|
block.entries.forEach((_, i) => out.push([...prefix, i]));
|
|
33
33
|
break;
|
|
34
34
|
case "table":
|
|
35
|
-
block.rows.forEach((row, r) => row.forEach((cell, c) => cell.blocks.forEach((inner, n) => blockPaths(inner, [...prefix, r, c, n], out))));
|
|
35
|
+
block.rows.forEach((row, r) => row.forEach((cell, c) => cell.blocks.forEach((inner, n) => blockPaths(inner, [...prefix, r, c, n], out, rules))));
|
|
36
36
|
break;
|
|
37
37
|
case "quote":
|
|
38
|
-
block.blocks.forEach((inner, n) => blockPaths(inner, [...prefix, n], out));
|
|
38
|
+
block.blocks.forEach((inner, n) => blockPaths(inner, [...prefix, n], out, rules));
|
|
39
|
+
break;
|
|
40
|
+
case "hr":
|
|
41
|
+
rules?.push({ path: prefix, before: out.length });
|
|
39
42
|
break;
|
|
40
|
-
case "hr": break;
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
const containerCache = new WeakMap();
|
|
@@ -50,6 +52,17 @@ export function containerPaths(doc) {
|
|
|
50
52
|
containerCache.set(doc, out);
|
|
51
53
|
return out;
|
|
52
54
|
}
|
|
55
|
+
const ruleCache = new WeakMap();
|
|
56
|
+
export function rulePaths(doc) {
|
|
57
|
+
const hit = ruleCache.get(doc);
|
|
58
|
+
if (hit)
|
|
59
|
+
return hit;
|
|
60
|
+
const out = [];
|
|
61
|
+
const containers = [];
|
|
62
|
+
doc.blocks.forEach((block, b) => blockPaths(block, [b], containers, out));
|
|
63
|
+
ruleCache.set(doc, out);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
53
66
|
export function blockContainers(block, prefix) {
|
|
54
67
|
const out = [];
|
|
55
68
|
blockPaths(block, prefix, out);
|
package/dist/plugin.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ export interface FileLike extends BlobLike {
|
|
|
69
69
|
/** Turns pasted or dropped files into blocks, or returns undefined if it does not handle them. */
|
|
70
70
|
export type FileHandler = (files: readonly FileLike[]) => readonly Block[] | undefined | Promise<readonly Block[] | undefined>;
|
|
71
71
|
/** What a plugin sees of the editor: the same object a host gets, nothing more. */
|
|
72
|
-
export type EditorHandle = Pick<Engine, "snapshot" | "apply" | "commands" | "subscribe" | "select" | "keymap" | "warn" | "importOptions" | "ui">;
|
|
72
|
+
export type EditorHandle = Pick<Engine, "snapshot" | "apply" | "commands" | "subscribe" | "select" | "keymap" | "warn" | "report" | "importOptions" | "ui">;
|
|
73
73
|
export interface PluginHost {
|
|
74
74
|
readonly editor: EditorHandle;
|
|
75
75
|
/** The editing operations, so a plugin does not import the editor to use them. */
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Elements that are dropped together with everything inside them. */
|
|
2
|
+
export declare const DROPPED_ELEMENTS: ReadonlySet<string>;
|
|
3
|
+
/** Elements that only group blocks: they end the paragraph before and after, and what is inside is kept. */
|
|
4
|
+
export declare const GROUPING_ELEMENTS: ReadonlySet<string>;
|
|
5
|
+
export declare const DEFAULT_LINK_SCHEMES: readonly string[];
|
|
6
|
+
/** Schemes an image address may use. Not configurable: a `data:` address is checked separately. */
|
|
7
|
+
export declare const IMAGE_SCHEMES: readonly string[];
|
|
8
|
+
/** The `data:` image types kept, with the first bytes (base64) their data must start with: the bytes are trusted, not the declared type. */
|
|
9
|
+
export declare const DATA_IMAGE_SIGNATURES: readonly (readonly [string, string])[];
|
|
10
|
+
/** The limits on what is read. */
|
|
11
|
+
export interface ContentLimits {
|
|
12
|
+
/** Longest markup read, in characters. Longer is refused whole: nothing is read and `onLimit` is told. */
|
|
13
|
+
readonly maxInputLength: number;
|
|
14
|
+
/** Deepest nesting of elements. An element nested deeper is dropped and what is inside it stays at the deepest allowed level. */
|
|
15
|
+
readonly maxDepth: number;
|
|
16
|
+
/** Longest `data:` image address, in characters. 0 refuses every `data:` image. */
|
|
17
|
+
readonly maxDataImageLength: number;
|
|
18
|
+
}
|
|
19
|
+
/** A profile: the limits and the choices a host makes. */
|
|
20
|
+
export interface ContentPolicy extends ContentLimits {
|
|
21
|
+
/** URL schemes a link may use besides relative and fragment addresses. */
|
|
22
|
+
readonly linkSchemes: readonly string[];
|
|
23
|
+
/** `semantic` keeps meaning only; `allowlist` also keeps text color, background and alignment. (Pasting is always `semantic`.) */
|
|
24
|
+
readonly styles: "semantic" | "allowlist";
|
|
25
|
+
/** Keep relative image addresses (`a.png`, `/a.png`) when pasting. Loading always keeps them. Default false in every profile. */
|
|
26
|
+
readonly pasteRelativeImages: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** The name of a profile in `CONTENT_PROFILES`. */
|
|
29
|
+
export type ContentProfile = "strict" | "standard" | "loose";
|
|
30
|
+
/** The three profiles. `standard` is the default; `strict` is for hosts that take content from people they do not trust; `loose` for hosts that trust theirs. */
|
|
31
|
+
export declare const CONTENT_PROFILES: Readonly<Record<ContentProfile, ContentPolicy>>;
|
|
32
|
+
/** What a host may say: a profile to start from, then single choices on top of it. */
|
|
33
|
+
export type ContentOptions = {
|
|
34
|
+
readonly profile?: ContentProfile;
|
|
35
|
+
} & Partial<ContentPolicy>;
|
|
36
|
+
/**
|
|
37
|
+
* The policy for the options: the profile (`standard` when none is named), with each given choice laid over it. A choice that
|
|
38
|
+
* cannot be used (an unknown profile, a limit that is not a whole number of at least 0, a list that is not strings) is left out, the
|
|
39
|
+
* profile's value is used, and `complain` is told.
|
|
40
|
+
*/
|
|
41
|
+
export declare function contentPolicy(options?: ContentOptions, complain?: (message: string) => void): ContentPolicy;
|