@rathkit/rte-core 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/autoformat/autoformat.js +18 -3
  3. package/dist/base64.js +5 -1
  4. package/dist/dom/index.d.ts +1 -1
  5. package/dist/dom/render.js +26 -15
  6. package/dist/dom/view.d.ts +59 -6
  7. package/dist/dom/view.js +195 -38
  8. package/dist/edit.d.ts +6 -0
  9. package/dist/edit.js +101 -11
  10. package/dist/engine.d.ts +26 -4
  11. package/dist/engine.js +113 -23
  12. package/dist/export.d.ts +2 -2
  13. package/dist/export.js +22 -6
  14. package/dist/footnotes/index.d.ts +2 -0
  15. package/dist/footnotes/index.js +2 -0
  16. package/dist/footnotes/plugin.d.ts +6 -0
  17. package/dist/footnotes/plugin.js +130 -0
  18. package/dist/footnotes/reconcile.d.ts +4 -0
  19. package/dist/footnotes/reconcile.js +155 -0
  20. package/dist/html/parse.d.ts +8 -2
  21. package/dist/html/parse.js +6 -2
  22. package/dist/images/images.js +4 -3
  23. package/dist/images/index.d.ts +5 -1
  24. package/dist/images/index.js +3 -1
  25. package/dist/images/plugin.js +2 -1
  26. package/dist/{dom/imageview.d.ts → images/view.d.ts} +9 -6
  27. package/dist/{dom/imageview.js → images/view.js} +20 -14
  28. package/dist/import.d.ts +18 -2
  29. package/dist/import.js +87 -42
  30. package/dist/index.d.ts +4 -1
  31. package/dist/index.js +3 -1
  32. package/dist/lists/lists.js +55 -30
  33. package/dist/markdown/detect.d.ts +1 -0
  34. package/dist/markdown/detect.js +81 -0
  35. package/dist/markdown/export.d.ts +10 -0
  36. package/dist/markdown/export.js +252 -0
  37. package/dist/markdown/index.d.ts +5 -0
  38. package/dist/markdown/index.js +4 -0
  39. package/dist/markdown/inline.d.ts +37 -0
  40. package/dist/markdown/inline.js +604 -0
  41. package/dist/markdown/parse.d.ts +4 -0
  42. package/dist/markdown/parse.js +586 -0
  43. package/dist/markdown/plugin.d.ts +20 -0
  44. package/dist/markdown/plugin.js +56 -0
  45. package/dist/model.d.ts +30 -2
  46. package/dist/model.js +2 -0
  47. package/dist/notes.d.ts +6 -0
  48. package/dist/notes.js +49 -0
  49. package/dist/ops.d.ts +6 -1
  50. package/dist/ops.js +5 -4
  51. package/dist/paste.js +9 -2
  52. package/dist/path.d.ts +10 -1
  53. package/dist/path.js +110 -11
  54. package/dist/plugin.d.ts +53 -2
  55. package/dist/policy.d.ts +41 -0
  56. package/dist/policy.js +40 -0
  57. package/dist/standard/index.d.ts +5 -3
  58. package/dist/standard/index.js +6 -5
  59. package/dist/stats.js +1 -1
  60. package/dist/svg/svg.js +9 -1
  61. package/dist/{dom/tableview.d.ts → tables/cells.d.ts} +7 -6
  62. package/dist/{dom/tableview.js → tables/cells.js} +16 -12
  63. package/dist/{dom/tablegrips.d.ts → tables/grips.d.ts} +5 -4
  64. package/dist/{dom/tablegrips.js → tables/grips.js} +10 -9
  65. package/dist/tables/index.d.ts +5 -1
  66. package/dist/tables/index.js +3 -1
  67. package/dist/tables/view.d.ts +2 -0
  68. package/dist/tables/view.js +15 -0
  69. package/dist/warning.d.ts +16 -0
  70. package/dist/warning.js +5 -0
  71. package/package.json +23 -1
package/dist/import.js CHANGED
@@ -1,22 +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
- import { COLOR_VALUE, COLUMN_WIDTH, MAX_INDENT, ROW_HEIGHT, inlineText, normalizeInline, } from "./model.js";
5
- const DROP = new Set([
6
- "script", "style", "link", "meta", "base", "object", "embed", "iframe", "frame", "frameset", "noscript", "template",
7
- "head", "textarea", "input", "select", "option", "button", "canvas", "audio", "video", "applet", "xml",
8
- ]);
9
- const CONTAINERS = new Set([
10
- "div", "section", "article", "header", "footer", "aside", "nav", "figure", "figcaption", "address", "main", "form",
11
- "body", "html", "center", "details", "summary", "dd", "dt", "fieldset", "hgroup",
12
- ]);
5
+ import { COLOR_VALUE, COLUMN_WIDTH, MAX_INDENT, NOTE_ID, ROW_HEIGHT, inlineText, normalizeInline, } from "./model.js";
6
+ import { asChildren } from "./path.js";
7
+ import { DEFAULT_MARKER } from "./model.js";
8
+ const DROP = DROPPED_ELEMENTS;
9
+ const CONTAINERS = GROUPING_ELEMENTS;
13
10
  const MARK_TAGS = {
14
11
  strong: "bold", b: "bold", em: "italic", i: "italic", cite: "italic", dfn: "italic", var: "italic", u: "underline", ins: "underline",
15
12
  s: "strike", strike: "strike", del: "strike", code: "code", tt: "code", kbd: "code", samp: "code", sup: "sup", sub: "sub",
16
13
  };
17
- const RASTER = [
18
- ["image/png", "iVBORw0KGgo"], ["image/jpeg", "/9j/"], ["image/gif", "R0lGOD"], ["image/webp", "UklGR"], ["image/bmp", "Qk"],
19
- ];
20
14
  const COLOR = COLOR_VALUE;
21
15
  export function parseStyle(style) {
22
16
  const out = new Map();
@@ -76,9 +70,11 @@ function safeImageSrc(raw, settings) {
76
70
  if (!match)
77
71
  return undefined;
78
72
  const body = match[2];
79
- return RASTER.some(([, magic]) => body.startsWith(magic)) ? value : undefined;
73
+ return DATA_IMAGE_SIGNATURES.some(([, magic]) => body.startsWith(magic)) ? value : undefined;
80
74
  }
81
- return safeUrl(value, new Set(["http", "https"]));
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));
82
78
  }
83
79
  class Sink {
84
80
  blocks = [];
@@ -275,6 +271,12 @@ function inlineOf(nodes, ctx, settings) {
275
271
  content.push(...(content.length > 0 ? [{ type: "br" }] : []), ...block.content);
276
272
  return normalizeInline(content);
277
273
  }
274
+ const NOTE_PREFIX = /^(?:user-content-)?(?:fnref|fn)[-:_]?/i;
275
+ const noteId = (raw, strip) => {
276
+ const id = strip ? raw?.replace(/^#/, "").replace(NOTE_PREFIX, "").replace(/[^\w-]/g, "") : raw;
277
+ return id !== undefined && NOTE_ID.test(id) ? id : undefined;
278
+ };
279
+ const hasClass = (el, name) => (el.attrs.class ?? "").split(/\s+/).includes(name);
278
280
  function convertElement(el, sink, ctx, settings) {
279
281
  const tag = el.tag;
280
282
  if (DROP.has(tag) || tag === "title")
@@ -291,7 +293,7 @@ function convertElement(el, sink, ctx, settings) {
291
293
  type: listItem.type,
292
294
  ...(listItem.start !== undefined && listItem.start !== 1 ? { start: listItem.start } : {}),
293
295
  ...(listItem.style !== undefined ? { style: listItem.style } : {}),
294
- items: [{ content: normalizeInline(listItem.content), sublists: [], level: listItem.level, listId: listItem.listId }],
296
+ items: [{ content: normalizeInline(listItem.content), children: [], level: listItem.level, listId: listItem.listId }],
295
297
  });
296
298
  }
297
299
  return;
@@ -304,6 +306,22 @@ function convertElement(el, sink, ctx, settings) {
304
306
  sink.flush((content) => ({ type: "h", level, content: withoutBold(content) }), true);
305
307
  return;
306
308
  }
309
+ if (tag === "a" && (el.attrs.role === "doc-backlink" || el.attrs["data-footnote-backref"] !== undefined || hasClass(el, "footnote-back") || hasClass(el, "footnote-backref")))
310
+ return;
311
+ if ((tag === "sup" && el.attrs["data-note"] !== undefined) || (tag === "a" && (el.attrs.role === "doc-noteref" || el.attrs["data-footnote-ref"] !== undefined || hasClass(el, "footnote-ref")))) {
312
+ const id = tag === "sup" ? noteId(el.attrs["data-note"], false) : noteId(el.attrs.href, true);
313
+ if (id !== undefined) {
314
+ sink.push({ type: "note", id });
315
+ return;
316
+ }
317
+ }
318
+ if (el.attrs.role === "doc-endnotes" || el.attrs["data-footnotes"] !== undefined || hasClass(el, "footnotes")) {
319
+ sink.flush();
320
+ for (const ol of el.tag === "ol" ? [el] : el.children)
321
+ if (ol.type === "el" && ol.tag === "ol")
322
+ sink.blocks.push(convertList(ol, ctx, settings, true));
323
+ return;
324
+ }
307
325
  switch (tag) {
308
326
  case "br":
309
327
  sink.push({ type: "br" });
@@ -452,18 +470,17 @@ function listStyleOf(el) {
452
470
  return style && /^[a-z-]+$/.test(style) && !/^(initial|inherit|unset|revert|none)$/.test(style) ? style : undefined;
453
471
  }
454
472
  const isCheckbox = (n) => n.type === "el" && n.tag === "input" && (n.attrs.type ?? "").toLowerCase() === "checkbox";
455
- const blankText = (n) => n.type === "text" && n.text.trim() === "";
456
473
  function taskOf(li) {
457
474
  const own = (li.attrs["aria-checked"] ?? li.attrs["data-checked"] ?? "").toLowerCase();
458
475
  const classes = (li.attrs.class ?? "").toLowerCase().split(/\s+/);
459
476
  const fromMarkup = own === "true" || classes.includes("checked") || classes.includes("task-done") ? true : own === "false" || classes.includes("unchecked") ? false : undefined;
460
477
  const kids = li.children;
461
- const at = kids.findIndex((n) => !blankText(n));
478
+ const at = kids.findIndex((n) => !isBlank(n));
462
479
  const first = at === -1 ? undefined : kids[at];
463
480
  if (first && isCheckbox(first))
464
481
  return { checked: "checked" in first.attrs, children: [...kids.slice(0, at), ...kids.slice(at + 1)] };
465
482
  if (first && first.type === "el" && /^(label|p|span|div)$/.test(first.tag)) {
466
- const inner = first.children.findIndex((n) => !blankText(n));
483
+ const inner = first.children.findIndex((n) => !isBlank(n));
467
484
  const box = inner === -1 ? undefined : first.children[inner];
468
485
  if (box && isCheckbox(box)) {
469
486
  const wrapper = { ...first, children: [...first.children.slice(0, inner), ...first.children.slice(inner + 1)] };
@@ -472,8 +489,10 @@ function taskOf(li) {
472
489
  }
473
490
  return fromMarkup === undefined ? undefined : { checked: fromMarkup, children: kids };
474
491
  }
475
- function convertList(el, ctx, settings) {
492
+ function convertList(el, ctx, settings, notes = false) {
476
493
  const list = { type: el.tag === "ol" ? "ol" : "ul", items: [] };
494
+ if (notes)
495
+ list.notes = true;
477
496
  const start = el.tag === "ol" && el.attrs.start !== undefined ? Number.parseInt(el.attrs.start, 10) : NaN;
478
497
  if (Number.isFinite(start) && start !== 1 && start >= 0 && start < 100000)
479
498
  list.start = start;
@@ -483,8 +502,8 @@ function convertList(el, ctx, settings) {
483
502
  continue;
484
503
  if (child.tag === "ul" || child.tag === "ol") {
485
504
  const nested = convertList(child, ctx, settings);
486
- const last = list.items[list.items.length - 1] ?? list.items[list.items.push({ content: [], sublists: [] }) - 1];
487
- last.sublists.push(nested);
505
+ const last = list.items[list.items.length - 1] ?? list.items[list.items.push({ content: [], children: [], bare: true }) - 1];
506
+ last.children.push(nested);
488
507
  continue;
489
508
  }
490
509
  if (child.tag !== "li")
@@ -497,19 +516,34 @@ function convertList(el, ctx, settings) {
497
516
  const task = taskOf(child);
498
517
  convertNodes(task ? task.children : child.children, inner, ctx, settings);
499
518
  inner.flush();
500
- const item = { content: [], sublists: [] };
519
+ const item = { content: [], children: [] };
501
520
  if (task)
502
521
  item.checked = task.checked;
522
+ let leading = true;
503
523
  for (const block of inner.blocks) {
504
- if (block.type === "ul" || block.type === "ol")
505
- item.sublists.push(block);
506
- else if (block.type === "p" || block.type === "h") {
524
+ if (leading && (block.type === "p" || block.type === "h")) {
507
525
  if (item.content.length > 0)
508
526
  item.content.push({ type: "br", paragraph: true });
509
527
  item.content.push(...block.content);
528
+ continue;
510
529
  }
530
+ leading = false;
531
+ if (isList(block))
532
+ item.children.push(block);
533
+ else
534
+ item.children.push(...asChildren([block]));
511
535
  }
512
536
  item.content = normalizeInline(item.content);
537
+ if (notes) {
538
+ const tail = item.content[item.content.length - 1];
539
+ if (tail?.type === "text")
540
+ item.content = normalizeInline([...item.content.slice(0, -1), { ...tail, text: tail.text.trimEnd() }]);
541
+ const id = noteId(child.attrs["data-note"], false) ?? noteId(child.attrs.id, true);
542
+ if (id !== undefined)
543
+ item.id = id;
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;
513
547
  const level = levelOf(child.attrs["data-aria-level"] ?? child.attrs["aria-level"]);
514
548
  if (level !== undefined)
515
549
  item.level = level;
@@ -637,7 +671,7 @@ function rowHeights(rowEls, kept) {
637
671
  return heights.some((h) => h !== null) ? heights : undefined;
638
672
  }
639
673
  function columnWidths(table, rows) {
640
- const cols = Math.max(0, ...rows.map((row) => row.reduce((sum, cell) => sum + cell.colspan, 0)));
674
+ const cols = rows.reduce((most, row) => Math.max(most, row.reduce((sum, cell) => sum + cell.colspan, 0)), 0);
641
675
  const widths = [];
642
676
  const visit = (nodes) => {
643
677
  for (const n of nodes) {
@@ -658,7 +692,7 @@ function columnWidths(table, rows) {
658
692
  if (firstRow)
659
693
  for (const cell of firstRow.children)
660
694
  if (cell.type === "el" && (cell.tag === "td" || cell.tag === "th")) {
661
- 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);
662
696
  if (span === 1)
663
697
  widths.push(widthOf(cell));
664
698
  else
@@ -693,13 +727,12 @@ function padRows(rows) {
693
727
  reach[k] = reach[k] - 1;
694
728
  return used;
695
729
  });
696
- const widest = Math.max(0, ...widths);
730
+ const widest = widths.reduce((most, w) => Math.max(most, w), 0);
697
731
  return rows.map((cells, i) => {
698
732
  const missing = widest - widths[i];
699
733
  return missing > 0 ? [...cells, ...Array.from({ length: missing }, () => ({ header: false, colspan: 1, rowspan: 1, blocks: [{ type: "p", content: [] }] }))] : cells;
700
734
  });
701
735
  }
702
- const DEFAULT_MARKER = (type, depth) => (type === "ol" ? "decimal" : ["disc", "circle", "square"][Math.min(depth, 2)]);
703
736
  const isList = (block) => block.type === "ul" || block.type === "ol";
704
737
  const hinted = (list) => list.items.length > 0 && list.items.every((item) => item.level !== undefined);
705
738
  function finishBlocks(blocks) {
@@ -744,8 +777,8 @@ function nestByLevel(lists) {
744
777
  const fresh = { type: list.type, items: [] };
745
778
  const parent = stack[stack.length - 1];
746
779
  if (parent) {
747
- const host = parent.items[parent.items.length - 1] ?? parent.items[parent.items.push({ content: [], sublists: [] }) - 1];
748
- host.sublists.push(fresh);
780
+ const host = parent.items[parent.items.length - 1] ?? parent.items[parent.items.push({ content: [], children: [], bare: true }) - 1];
781
+ host.children.push(fresh);
749
782
  }
750
783
  else {
751
784
  result.push(fresh);
@@ -757,15 +790,15 @@ function nestByLevel(lists) {
757
790
  stack.push(fresh);
758
791
  ids.push(item.listId);
759
792
  }
760
- stack[stack.length - 1].items.push({ content: item.content, sublists: item.sublists, ...(item.checked !== undefined ? { checked: item.checked } : {}) });
793
+ stack[stack.length - 1].items.push({ content: item.content, children: item.children, ...(item.checked !== undefined ? { checked: item.checked } : {}) });
761
794
  }
762
795
  }
763
796
  return result.map((l) => freezeList(l, 0));
764
797
  }
765
798
  function freezeList(list, depth) {
766
- const items = list.items.map((item) => ({ content: item.content, sublists: item.sublists.map((sub) => freezeList(sub, depth + 1)), ...(item.checked !== undefined ? { checked: item.checked } : {}) }));
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 } : {}) }));
767
800
  const style = list.style !== undefined && list.style !== DEFAULT_MARKER(list.type, depth) ? list.style : undefined;
768
- return { type: list.type, ...(list.start !== undefined ? { start: list.start } : {}), ...(style !== undefined ? { style } : {}), items };
801
+ return { type: list.type, ...(list.notes ? { notes: true } : {}), ...(list.start !== undefined ? { start: list.start } : {}), ...(style !== undefined ? { style } : {}), items };
769
802
  }
770
803
  function repairRewrittenSpaces(nodes) {
771
804
  return nodes.map((node) => {
@@ -784,17 +817,29 @@ function repairRewrittenSpaces(nodes) {
784
817
  export function importBlocks(html, options = {}) {
785
818
  const settings = {
786
819
  allowStyles: (options.styles ?? "allowlist") === "allowlist",
787
- schemes: new Set(options.linkSchemes ?? ["http", "https", "mailto", "tel"]),
820
+ schemes: new Set(options.linkSchemes ?? DEFAULT_LINK_SCHEMES),
788
821
  keepEmpty: options.keepEmptyBlocks ?? false,
789
- maxData: options.maxDataImageLength ?? 12_000_000,
822
+ maxData: options.maxDataImageLength ?? CONTENT_PROFILES.standard.maxDataImageLength,
823
+ relative: options.relativeImages ?? true,
790
824
  svg: options.sanitizeSvg,
791
825
  recognizers: options.recognizers ?? [],
792
826
  };
793
- const sink = new Sink(settings);
794
- const tree = parseHtml(html);
795
- convertNodes(/-webkit-text-stroke-width/.test(html) ? repairRewrittenSpaces(tree) : tree, sink, { marks: [], inLink: false, pre: false }, settings);
796
- sink.flush();
797
- return finishBlocks(sink.blocks);
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
+ }
798
843
  }
799
844
  export function importHtml(html, options = {}) {
800
845
  const blocks = importBlocks(html, { keepEmptyBlocks: true, ...options });
@@ -817,4 +862,4 @@ export function importText(value) {
817
862
  }
818
863
  const decodeLineBreaks = (value) => value.replace(/\r\n?|\u2028|\u2029/g, "\n").replace(/^\n+|\n+$/g, "");
819
864
  export { decodeEntities };
820
- 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,9 +1,12 @@
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";
8
+ export type { CopyFormatter, Normalizer } from "./plugin.ts";
9
+ export { isNotes, noteIds } from "./notes.ts";
7
10
  export { defaultPaste, extractFragment, pasteBlocks, type ClipboardData, type PasteHandler } from "./paste.ts";
8
11
  export { decodeBase64, encodeBase64 } from "./base64.ts";
9
12
  export { createEngine } from "./core.ts";
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
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";
8
+ export { isNotes, noteIds } from "./notes.js";
7
9
  export { defaultPaste, extractFragment, pasteBlocks } from "./paste.js";
8
10
  export { decodeBase64, encodeBase64 } from "./base64.js";
9
11
  export { createEngine } from "./core.js";
@@ -2,13 +2,28 @@ import { deleteSelection, sliceInline } from "../edit.js";
2
2
  import { inlineText, normalizeInline } from "../model.js";
3
3
  import { caret, clampPos, containerPaths, selectedContainers, fixup, getBlock, getContent, isCollapsed, kindAt, mapBlockList, pathKey, setContent, } from "../path.js";
4
4
  function locateItem(doc, path) {
5
+ const inList = (list, rest, listBlock, steps) => {
6
+ const [i, k, ...more] = rest;
7
+ if (i === undefined)
8
+ return undefined;
9
+ if (k === undefined)
10
+ return { listBlock, item: [...steps, i] };
11
+ const child = list.items[i]?.children[k];
12
+ if (!child)
13
+ return undefined;
14
+ if (child.type === "ul" || child.type === "ol")
15
+ return inList(child, more, listBlock, [...steps, i, k]);
16
+ if (child.type === "quote")
17
+ return walk(child.blocks, more, [...listBlock, ...steps, i, k]);
18
+ return undefined;
19
+ };
5
20
  const walk = (blocks, rest, prefix) => {
6
21
  const [n, ...more] = rest;
7
22
  const block = n === undefined ? undefined : blocks[n];
8
23
  if (!block)
9
24
  return undefined;
10
25
  if (block.type === "ul" || block.type === "ol")
11
- return { listBlock: [...prefix, n], item: more };
26
+ return inList(block, more, [...prefix, n], []);
12
27
  if (block.type === "quote")
13
28
  return walk(block.blocks, more, [...prefix, n]);
14
29
  if (block.type === "table") {
@@ -30,8 +45,8 @@ function listOfItem(doc, path) {
30
45
  for (let at = 0;; at += 2) {
31
46
  if (steps.length - at === 1)
32
47
  return { list, index: steps[at] };
33
- const sub = list.items[steps[at]]?.sublists[steps[at + 1]];
34
- if (!sub)
48
+ const sub = list.items[steps[at]]?.children[steps[at + 1]];
49
+ if (sub?.type !== "ul" && sub?.type !== "ol")
35
50
  return undefined;
36
51
  list = sub;
37
52
  }
@@ -53,21 +68,21 @@ function splitList(list, path, liftChildren) {
53
68
  const item = list.items[i];
54
69
  const numbering = (count) => (list.type === "ol" ? { start: (list.start ?? 1) + count } : {});
55
70
  if (k === undefined) {
56
- const placeholder = item.sublists.length > 0 && !liftChildren ? [{ content: [], sublists: item.sublists }] : [];
71
+ const placeholder = item.children.length > 0 && !liftChildren ? [{ content: [], children: item.children, bare: true }] : [];
57
72
  const rest = [...placeholder, ...list.items.slice(i + 1)];
58
73
  return {
59
74
  before: i > 0 ? { ...list, items: list.items.slice(0, i) } : undefined,
60
75
  content: item.content,
61
- lifted: liftChildren ? item.sublists : [],
76
+ lifted: liftChildren ? item.children : [],
62
77
  after: rest.length > 0 ? { ...list, ...numbering(i + 1), items: rest } : undefined,
63
78
  };
64
79
  }
65
- const inner = splitList(item.sublists[k], path.slice(2), liftChildren);
66
- const keptSublists = [...item.sublists.slice(0, k), ...(inner.before ? [inner.before] : [])];
67
- const afterSublists = [...(inner.after ? [inner.after] : []), ...item.sublists.slice(k + 1)];
68
- const rest = [...(afterSublists.length > 0 ? [{ content: [], sublists: afterSublists }] : []), ...list.items.slice(i + 1)];
80
+ const inner = splitList(item.children[k], path.slice(2), liftChildren);
81
+ const keptSublists = [...item.children.slice(0, k), ...(inner.before ? [inner.before] : [])];
82
+ const afterSublists = [...(inner.after ? [inner.after] : []), ...item.children.slice(k + 1)];
83
+ const rest = [...(afterSublists.length > 0 ? [{ content: [], children: afterSublists, bare: true }] : []), ...list.items.slice(i + 1)];
69
84
  return {
70
- before: { ...list, items: [...list.items.slice(0, i), { content: item.content, sublists: keptSublists }] },
85
+ before: { ...list, items: [...list.items.slice(0, i), { content: item.content, children: keptSublists }] },
71
86
  content: inner.content,
72
87
  lifted: inner.lifted,
73
88
  after: rest.length > 0 ? { ...list, ...numbering(i + 1), items: rest } : undefined,
@@ -98,7 +113,7 @@ function updateListOf(doc, itemPath, change) {
98
113
  if (steps.length === 1)
99
114
  return change(list);
100
115
  const [i, k] = steps;
101
- return { ...list, items: list.items.map((item, idx) => (idx !== i ? item : { ...item, sublists: item.sublists.map((sub, s) => (s === k ? edit(sub, steps.slice(2)) : sub)) })) };
116
+ return { ...list, items: list.items.map((item, idx) => (idx !== i ? item : { ...item, children: item.children.map((sub, s) => (s === k && (sub.type === "ul" || sub.type === "ol") ? edit(sub, steps.slice(2)) : sub)) })) };
102
117
  };
103
118
  return replaceBlock(doc, loc.listBlock, [edit(root, loc.item)]);
104
119
  }
@@ -179,7 +194,7 @@ export function toggleList(doc, sel, type, options = {}) {
179
194
  for (const run of [...runs].reverse()) {
180
195
  const first = run[0];
181
196
  next = fixup(mapBlockList(next, first, (blocks, index) => {
182
- const items = run.map((_, n) => ({ content: blocks[index + n].content, sublists: [] }));
197
+ const items = run.map((_, n) => ({ content: blocks[index + n].content, children: [] }));
183
198
  let list = withOptions({ type, items }, options);
184
199
  let from = index;
185
200
  let to = index + run.length;
@@ -218,7 +233,7 @@ function mapListAt(doc, itemPath, fn) {
218
233
  const [i, k] = steps;
219
234
  return {
220
235
  ...list,
221
- items: list.items.map((item, idx) => idx !== i ? item : { ...item, sublists: item.sublists.flatMap((sub, s) => (s !== k ? [sub] : [edit(sub, steps.slice(2))].filter((x) => x !== undefined))) }),
236
+ items: list.items.map((item, idx) => idx !== i ? item : { ...item, children: item.children.flatMap((sub, s) => (s !== k || (sub.type !== "ul" && sub.type !== "ol") ? [sub] : [edit(sub, steps.slice(2))].filter((x) => x !== undefined))) }),
222
237
  };
223
238
  };
224
239
  const next = edit(root, loc.item);
@@ -247,16 +262,16 @@ export function indentItems(doc, sel, maxDepth = Number.POSITIVE_INFINITY) {
247
262
  const flat = moved;
248
263
  const previous = list.items[index - 1];
249
264
  const nested = (target) => {
250
- const last = target.sublists.at(-1);
251
- const sublists = last && last.type === list.type
252
- ? [...target.sublists.slice(0, -1), { ...last, items: [...last.items, ...flat] }]
253
- : [...target.sublists, { type: list.type, items: flat }];
254
- return { ...target, sublists };
265
+ const last = target.children.at(-1);
266
+ const children = last && last.type === list.type
267
+ ? [...target.children.slice(0, -1), { ...last, items: [...last.items, ...flat] }]
268
+ : [...target.children, { type: list.type, items: flat }];
269
+ return { ...target, children };
255
270
  };
256
271
  const rest = list.items.slice(index + group.length);
257
272
  if (previous)
258
273
  return { ...list, items: [...list.items.slice(0, index - 1), nested(previous), ...rest] };
259
- return { ...list, items: [nested({ content: [], sublists: [] }), ...rest] };
274
+ return { ...list, items: [nested({ content: [], children: [], bare: true }), ...rest] };
260
275
  });
261
276
  }
262
277
  return { doc: next, selection: carry(doc, next, sel) };
@@ -271,6 +286,10 @@ export function outdentItems(doc, sel) {
271
286
  if (!loc)
272
287
  continue;
273
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;
274
293
  next = takeOutOfList(next, path, true);
275
294
  continue;
276
295
  }
@@ -279,16 +298,17 @@ export function outdentItems(doc, sel) {
279
298
  const itemIndex = path.at(-1);
280
299
  next = mapListAt(next, parentPath, (list, parentIndex) => {
281
300
  const parent = list.items[parentIndex];
282
- const sub = parent.sublists[sublistIndex];
301
+ const sub = parent.children[sublistIndex];
283
302
  const moved = sub.items[itemIndex];
284
303
  const followers = sub.items.slice(itemIndex + 1);
285
304
  const keptSub = itemIndex > 0 ? [{ ...sub, items: sub.items.slice(0, itemIndex) }] : [];
286
305
  const lifted = {
287
306
  content: moved.content,
288
- sublists: [...moved.sublists, ...(followers.length > 0 ? [{ type: sub.type, ...(sub.style !== undefined ? { style: sub.style } : {}), items: followers }] : [])],
307
+ ...(moved.bare ? { bare: true } : {}),
308
+ children: [...moved.children, ...(followers.length > 0 ? [{ type: sub.type, ...(sub.style !== undefined ? { style: sub.style } : {}), items: followers }] : [])],
289
309
  };
290
- const sublists = [...parent.sublists.slice(0, sublistIndex), ...keptSub, ...parent.sublists.slice(sublistIndex + 1)];
291
- const items = [...list.items.slice(0, parentIndex), { content: parent.content, sublists }, lifted, ...list.items.slice(parentIndex + 1)];
310
+ const children = [...parent.children.slice(0, sublistIndex), ...keptSub, ...parent.children.slice(sublistIndex + 1)];
311
+ const items = [...list.items.slice(0, parentIndex), { content: parent.content, children }, lifted, ...list.items.slice(parentIndex + 1)];
292
312
  return { ...list, items };
293
313
  });
294
314
  }
@@ -340,6 +360,11 @@ export function backspaceAtItemStart(doc, pos) {
340
360
  const held = listOfItem(doc, pos.path);
341
361
  if (held?.list.items[held.index]?.checked !== undefined)
342
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
+ }
343
368
  if (!held || held.index !== 0)
344
369
  return undefined;
345
370
  return leaveListItem(doc, pos);
@@ -351,7 +376,7 @@ export function splitListItem(doc, pos) {
351
376
  const item = held?.list.items[held.index];
352
377
  if (!loc || !item)
353
378
  return undefined;
354
- if (content.length === 0 && item.sublists.length === 0)
379
+ if (content.length === 0 && item.children.length === 0)
355
380
  return leaveListItem(doc, pos);
356
381
  const before = sliceInline(content, 0, pos.offset);
357
382
  const after = sliceInline(content, pos.offset);
@@ -359,8 +384,8 @@ export function splitListItem(doc, pos) {
359
384
  const next = mapListAt(doc, pos.path, (list, index) => ({
360
385
  ...list,
361
386
  items: list.items.flatMap((candidate, i) => (i !== index ? [candidate] : [
362
- { content: before, sublists: [], ...(item.checked !== undefined ? { checked: item.checked } : {}) },
363
- { content: after, sublists: item.sublists, ...(item.checked !== undefined ? { checked: false } : {}) },
387
+ { content: before, children: [], ...(item.checked !== undefined ? { checked: item.checked } : {}), ...(item.id !== undefined ? { id: item.id } : {}) },
388
+ { content: after, children: item.children, ...(item.checked !== undefined ? { checked: false } : {}), ...(item.bare && after.length === 0 ? { bare: true } : {}) },
364
389
  ])),
365
390
  }));
366
391
  return { doc: next, selection: caret({ path: containerPaths(next)[ordinal + 1] ?? pos.path, offset: 0 }) };
@@ -377,11 +402,11 @@ export function insertIntoList(doc, pos, blocks, before, after) {
377
402
  if (!item)
378
403
  return list;
379
404
  const rows = others.length === 0
380
- ? [{ content: normalizeInline([...before, ...first, ...after]), sublists: item.sublists }]
405
+ ? [{ ...item, content: normalizeInline([...before, ...first, ...after]) }]
381
406
  : [
382
- { content: normalizeInline([...before, ...first]), sublists: [], ...(item.checked !== undefined ? { checked: item.checked } : {}) },
383
- ...others.slice(0, -1).map((content) => ({ content, sublists: [], ...(item.checked !== undefined ? { checked: false } : {}) })),
384
- { content: normalizeInline([...lastText, ...after]), sublists: item.sublists, ...(item.checked !== undefined ? { checked: false } : {}) },
407
+ { content: normalizeInline([...before, ...first]), children: [], ...(item.checked !== undefined ? { checked: item.checked } : {}), ...(item.id !== undefined ? { id: item.id } : {}) },
408
+ ...others.slice(0, -1).map((content) => ({ content, children: [], ...(item.checked !== undefined ? { checked: false } : {}) })),
409
+ { content: normalizeInline([...lastText, ...after]), children: item.children, ...(item.checked !== undefined ? { checked: false } : {}) },
385
410
  ];
386
411
  landing = rows.length - 1;
387
412
  return { ...list, items: [...list.items.slice(0, index), ...rows, ...list.items.slice(index + 1)] };
@@ -0,0 +1 @@
1
+ export declare function looksLikeMarkdown(input: string): boolean;
@@ -0,0 +1,81 @@
1
+ const LIMIT = 200_000;
2
+ const LINE_LIMIT = 2_000;
3
+ const isList = (line) => /^ {0,6}(?:[-*+]|\d{1,9}[.)])[ \t]+\S/.test(line.slice(0, 64));
4
+ const isRow = (line) => line.includes("|") && /^[\s|:-]+$/.test(line) && line.includes("-");
5
+ function pair(text, mark) {
6
+ let from = 0;
7
+ for (let tries = 0; tries < 50; tries++) {
8
+ const a = text.indexOf(mark, from);
9
+ if (a === -1)
10
+ return false;
11
+ const inner = a + mark.length;
12
+ if (!/\s/.test(text.charAt(inner)) && inner < text.length) {
13
+ const b = text.indexOf(mark, inner + 1);
14
+ if (b !== -1 && !/\s/.test(text.charAt(b - 1)) && text.slice(inner, b).indexOf("\n\n") === -1)
15
+ return true;
16
+ }
17
+ from = a + mark.length;
18
+ }
19
+ return false;
20
+ }
21
+ function link(text) {
22
+ let from = 0;
23
+ for (let tries = 0; tries < 50; tries++) {
24
+ const mid = text.indexOf("](", from);
25
+ if (mid === -1)
26
+ return false;
27
+ const open = text.lastIndexOf("[", mid);
28
+ const close = text.indexOf(")", mid);
29
+ const label = open === -1 ? "" : text.slice(open + 1, mid);
30
+ if (open !== -1 && close !== -1 && label !== "" && !label.includes("\n") && close - mid < 2000)
31
+ return true;
32
+ from = mid + 2;
33
+ }
34
+ return false;
35
+ }
36
+ export function looksLikeMarkdown(input) {
37
+ const text = input.length > LIMIT ? input.slice(0, LIMIT) : input;
38
+ const lines = text.split(/\r\n?|\n/);
39
+ let headings = 0, listLines = 0, tasks = 0, quotes = 0, notes = 0;
40
+ let fence = false, table = false;
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const line = lines[i].slice(0, LINE_LIMIT);
43
+ if (/^ {0,3}(?:`{3,}|~{3,})/.test(line))
44
+ fence = true;
45
+ else if (/^ {0,3}#{1,6}[ \t]+\S/.test(line))
46
+ headings++;
47
+ else if (/^ {0,3}>/.test(line))
48
+ quotes++;
49
+ else if (/^ {0,3}\[\^[^\]\s]{1,64}\]:[ \t]*\S/.test(line))
50
+ notes++;
51
+ else if (isList(line)) {
52
+ listLines++;
53
+ if (/^\s*(?:[-*+]|\d+[.)])[ \t]+\[[ xX]\][ \t]/.test(line))
54
+ tasks++;
55
+ }
56
+ else if (line.includes("|") && i + 1 < lines.length && isRow(lines[i + 1].slice(0, LINE_LIMIT)) && line.replace(/[\s|]/g, "") !== "")
57
+ table = true;
58
+ }
59
+ let score = 0;
60
+ if (fence)
61
+ score += 3;
62
+ if (table)
63
+ score += 3;
64
+ if (headings > 0)
65
+ score += headings > 1 ? 2 : 1;
66
+ if (listLines >= 2)
67
+ score += 2;
68
+ if (tasks > 0)
69
+ score += 2;
70
+ if (quotes > 0)
71
+ score += 1;
72
+ if (notes > 0)
73
+ score += 2;
74
+ if (link(text))
75
+ score += 2;
76
+ if (pair(text, "**") || pair(text, "~~"))
77
+ score += 2;
78
+ if (pair(text, "`"))
79
+ score += 1;
80
+ return score >= 2;
81
+ }
@@ -0,0 +1,10 @@
1
+ import type { Doc } from "../model.ts";
2
+ /** The document as Markdown (CommonMark with the GitHub table, task item and strikethrough additions). */
3
+ export declare function exportMarkdown(doc: Doc, options?: MarkdownExportOptions): string;
4
+ export interface MarkdownExportOptions {
5
+ /**
6
+ * Write underline, superscript and subscript as `<u>`, `<sup>` and `<sub>`, and a line break in a table cell or heading as `<br>`. The
7
+ * reader here understands those tags, so they come back; other Markdown readers show them as HTML. Default false: those details are dropped.
8
+ */
9
+ readonly inlineHtml?: boolean;
10
+ }