@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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @rathkit/rte-core
2
2
 
3
- > **Alpha** (0.1.0-alpha.0). Not for production: the APIs will change between releases. Published to the `next` tag only.
3
+ > **Alpha.** Not for production: the APIs will change between releases. Published to the `next` tag only.
4
4
 
5
5
  The editor itself: the document model, HTML import and export, editing operations, commands and keys, the plugin host, the browser view, and the free plugins (lists, tables, find and replace, word-processor lists, SVG, images). The model does not touch the DOM, so it also runs in Node.
6
6
 
@@ -73,7 +73,19 @@ export function spanShortcut(doc, sel, typed) {
73
73
  }
74
74
  return undefined;
75
75
  }
76
- const ADDRESS = /(?:https?:\/\/|www\.)[^\s<>"]+$/i;
76
+ const ADDRESS_START = /(?:https?:\/\/|www\.)/gi;
77
+ const ADDRESS_BREAK = /[\s<>"]/;
78
+ function addressAtEnd(text) {
79
+ let from = text.length;
80
+ while (from > 0 && !ADDRESS_BREAK.test(text[from - 1]))
81
+ from--;
82
+ ADDRESS_START.lastIndex = from;
83
+ for (let m = ADDRESS_START.exec(text); m; m = ADDRESS_START.exec(text)) {
84
+ if (m.index + m[0].length < text.length)
85
+ return { index: m.index, text: text.slice(m.index) };
86
+ }
87
+ return undefined;
88
+ }
77
89
  export function addressShortcut(doc, sel, typed) {
78
90
  if (typed !== " " || !isCollapsed(sel))
79
91
  return undefined;
@@ -83,10 +95,13 @@ export function addressShortcut(doc, sel, typed) {
83
95
  const content = getContent(doc, path);
84
96
  const offset = sel.anchor.offset;
85
97
  const before = inlineText(sliceInline(content, 0, offset));
86
- const m = ADDRESS.exec(before);
98
+ const m = addressAtEnd(before);
87
99
  if (!m)
88
100
  return undefined;
89
- const word = m[0].replace(/[.,;:!?)\]'"]+$/, "");
101
+ let stop = m.text.length;
102
+ while (stop > 0 && ".,;:!?)]'\"".includes(m.text[stop - 1]))
103
+ stop--;
104
+ const word = m.text.slice(0, stop);
90
105
  if (word.replace(/^(?:https?:\/\/|www\.)/i, "").length < 3 || !/[a-z0-9]/i.test(word.replace(/^(?:https?:\/\/|www\.)/i, "")))
91
106
  return undefined;
92
107
  const start = m.index;
package/dist/base64.js CHANGED
@@ -1,6 +1,10 @@
1
1
  const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2
2
  export function decodeBase64(input) {
3
- const text = input.replace(/\s+/g, "").replace(/=+$/, "");
3
+ const spaced = input.replace(/\s+/g, "");
4
+ let end = spaced.length;
5
+ while (end > 0 && spaced[end - 1] === "=")
6
+ end--;
7
+ const text = spaced.slice(0, end);
4
8
  if (/[^A-Za-z0-9+/]/.test(text))
5
9
  return undefined;
6
10
  const out = new Uint8Array(Math.floor((text.length * 3) / 4));
@@ -1,2 +1,2 @@
1
- export { Editor, type EditorOptions } from "./view.ts";
1
+ export { Editor, type EditorEvents, type EditorOptions } from "./view.ts";
2
2
  export { renderDoc, posFromDom, domFromPos } from "./render.ts";
@@ -44,6 +44,13 @@ function renderInline(parent, content, options) {
44
44
  parent.appendChild(gap);
45
45
  }
46
46
  }
47
+ else if (node.type === "note") {
48
+ const mark = document.createElement("sup");
49
+ mark.setAttribute("data-rte-note", node.id);
50
+ mark.setAttribute("contenteditable", "false");
51
+ mark.setAttribute("role", "doc-noteref");
52
+ parent.appendChild(mark);
53
+ }
47
54
  else if (node.type === "image") {
48
55
  const img = document.createElement("img");
49
56
  if (isRemote(node.src) && !options.loadRemoteImages)
@@ -104,14 +111,21 @@ function renderList(list, path, options) {
104
111
  el.setAttribute("start", String(list.start));
105
112
  if (list.style !== undefined)
106
113
  el.style.listStyleType = list.style;
114
+ if (list.notes) {
115
+ el.setAttribute("data-rte-notes", "");
116
+ el.setAttribute("role", "doc-endnotes");
117
+ el.setAttribute("aria-label", "Notes");
118
+ }
107
119
  list.items.forEach((item, i) => {
108
120
  const itemPath = [...path, i];
109
121
  const li = tag("li", itemPath);
110
- if (item.content.length === 0 && item.sublists.length > 0)
122
+ if (list.notes && item.id)
123
+ li.setAttribute("data-rte-note-id", item.id);
124
+ if (item.bare === true && item.content.length === 0 && (item.children[0]?.type === "ul" || item.children[0]?.type === "ol"))
111
125
  li.style.listStyleType = "none";
112
126
  if (item.checked !== undefined) {
113
127
  li.style.listStyleType = "none";
114
- li.setAttribute("data-rk-task", item.checked ? "done" : "open");
128
+ li.setAttribute("data-rte-task", item.checked ? "done" : "open");
115
129
  const box = document.createElement("span");
116
130
  box.setAttribute("data-rte-check", "");
117
131
  box.setAttribute("role", "checkbox");
@@ -121,7 +135,7 @@ function renderList(list, path, options) {
121
135
  li.appendChild(box);
122
136
  }
123
137
  renderInline(li, item.content, options);
124
- item.sublists.forEach((sub, k) => li.appendChild(renderList(sub, [...itemPath, k], options)));
138
+ item.children.forEach((child, k) => li.appendChild(renderBlock(child, [...itemPath, k], options)));
125
139
  el.appendChild(li);
126
140
  });
127
141
  return el;
@@ -281,17 +295,14 @@ export function renderDoc(doc, root, options = { loadRemoteImages: false }, cach
281
295
  });
282
296
  cache.dirty.clear();
283
297
  cache.claimed.clear();
284
- const keep = new Set(wanted);
285
- for (const node of [...root.childNodes])
286
- if (!keep.has(node))
287
- root.removeChild(node);
288
- let cursor = root.firstChild;
289
- for (const el of wanted) {
290
- if (cursor === el)
291
- cursor = cursor.nextSibling;
292
- else
293
- root.insertBefore(el, cursor);
294
- }
298
+ reconcile(root, wanted);
299
+ root.querySelectorAll("sup[data-rte-note]").forEach((mark, i) => {
300
+ const n = String(i + 1);
301
+ if (mark.textContent !== n) {
302
+ mark.textContent = n;
303
+ mark.setAttribute("aria-label", `Footnote ${n}`);
304
+ }
305
+ });
295
306
  }
296
307
  function* inlineItems(container) {
297
308
  const stack = [[...container.childNodes].reverse()];
@@ -307,7 +318,7 @@ function* inlineItems(container) {
307
318
  else if (node instanceof Element) {
308
319
  if (node.hasAttribute("data-rte-ph") || node.hasAttribute("data-rte-check") || node.hasAttribute("data-rte-caption") || BLOCK_TAGS.has(node.tagName))
309
320
  continue;
310
- if (node.tagName === "BR" || node.tagName === "IMG")
321
+ if (node.tagName === "BR" || node.tagName === "IMG" || node.hasAttribute("data-rte-note"))
311
322
  yield node;
312
323
  else
313
324
  stack.push([...node.childNodes].reverse());
@@ -1,5 +1,7 @@
1
1
  import type { Engine } from "../engine.ts";
2
2
  import type { Plugin } from "../plugin.ts";
3
+ import type { ContentOptions } from "../policy.ts";
4
+ import type { Warning } from "../warning.ts";
3
5
  import { type Doc, type MarkType } from "../model.ts";
4
6
  import { type ClipboardData, type PasteHandler } from "../paste.ts";
5
7
  import { type Pos, type Selection } from "../path.ts";
@@ -7,7 +9,10 @@ export interface EditorOptions {
7
9
  readonly element: HTMLElement;
8
10
  readonly html?: string;
9
11
  readonly pasteHandlers?: readonly PasteHandler[];
12
+ /** No edits; the page can still be focused, selected and copied from. Default false. */
10
13
  readonly readOnly?: boolean;
14
+ /** Read-only, and also out of the tab order and not focusable: for a field that does not apply now. Default false. */
15
+ readonly disabled?: boolean;
11
16
  /** Draw remote (`http`, `https`) images. Off by default, so pasting never makes the browser fetch anything. */
12
17
  readonly loadRemoteImages?: boolean;
13
18
  /** Called with the `inputType` of input the editor does not handle. That input is blocked, not applied. */
@@ -15,12 +20,16 @@ export interface EditorOptions {
15
20
  /** Plugins to add: lists, for one (`@rathkit/rte-core/lists`). With none, the editor has only what is built in. */
16
21
  readonly plugins?: readonly Plugin[];
17
22
  /**
18
- * A hint shown while the document is empty. The editor marks itself `data-rk-empty` when empty, says the hint to assistive
19
- * technology (`aria-placeholder`) and puts it in the custom property `--rk-placeholder`; the styled interface draws it.
23
+ * A hint shown while the document is empty. The editor marks itself `data-rte-empty` when empty, says the hint to assistive
24
+ * technology (`aria-placeholder`) and puts it in the custom property `--rte-placeholder`; the styled interface draws it.
20
25
  */
21
26
  readonly placeholder?: string;
22
27
  /** The most text the document may hold, in characters. Typing and pasting past it are refused (see `onMaxLength`). */
23
28
  readonly maxLength?: number;
29
+ /** Told about problems the editor carried on from (a plugin that threw, content that was refused): see docs/ERRORS.md. Default: the console. */
30
+ readonly onWarning?: (warning: Warning) => void;
31
+ /** What is kept and dropped when HTML is read: a profile and single choices on top of it. See docs/CONTENT-POLICY.md. */
32
+ readonly content?: ContentOptions;
24
33
  /** Called when an edit is refused for being past `maxLength`, with the limit and what the length would have been. */
25
34
  readonly onMaxLength?: (info: {
26
35
  readonly limit: number;
@@ -32,12 +41,29 @@ export interface EditorOptions {
32
41
  readonly autofocus?: boolean;
33
42
  }
34
43
  type Listener = (editor: Editor) => void;
44
+ /**
45
+ * What an editor reports, by name (`editor.on(name, listener)`). Every package that wraps the editor reports the same five things
46
+ * with the same timing; see docs/EVENTS.md.
47
+ * - `change`: the document's HTML, after an edit that altered it. Not for `setHtml`, and not for a selection that moved.
48
+ * - `focus` and `blur`: the editor's own focus moved in or out.
49
+ * - `selection`: the selection moved or changed.
50
+ */
51
+ export interface EditorEvents {
52
+ change: (html: string) => void;
53
+ focus: (editor: Editor) => void;
54
+ blur: (editor: Editor) => void;
55
+ selection: (selection: Selection) => void;
56
+ }
35
57
  export declare class Editor {
36
58
  readonly element: HTMLElement;
37
59
  /** The editor's state, commands, keys and plugins. It has no DOM, so everything it does can be driven without a page. */
38
60
  readonly engine: Engine;
39
61
  private composing;
40
62
  private reading;
63
+ private userReadOnly;
64
+ private isDisabled;
65
+ private ownTabindex;
66
+ private savedTabindex;
41
67
  private readonly loadRemoteImages;
42
68
  private readonly unhandled;
43
69
  private dragSource;
@@ -45,9 +71,12 @@ export declare class Editor {
45
71
  /** Notices changes to the page that the editor did not make, so the block they touched is drawn again from the model. */
46
72
  private readonly observer;
47
73
  private readonly cleanups;
48
- private readonly cellMarks;
49
- private readonly imageSelection;
50
- private readonly tableGrips;
74
+ private readonly changeListeners;
75
+ /** Set while `setHtml` replaces the document, so replacing it is not reported as an edit. */
76
+ private replacing;
77
+ /** Lets each `change` listener forget what it last reported, after the document was replaced. */
78
+ private readonly resyncs;
79
+ private readonly viewHost;
51
80
  constructor(options: EditorOptions);
52
81
  get doc(): Doc;
53
82
  get selection(): Selection;
@@ -57,7 +86,10 @@ export declare class Editor {
57
86
  rangeFor(from: Pos, to: Pos): Range | null;
58
87
  get canUndo(): boolean;
59
88
  get canRedo(): boolean;
89
+ /** Whether edits are refused: read-only, or disabled. */
60
90
  get readOnly(): boolean;
91
+ /** Whether the editor is disabled: read-only and out of the tab order. */
92
+ get disabled(): boolean;
61
93
  /** Named commands and their state: `editor.commands.run("format.bold")`, `editor.commands.state("format.bold")`. */
62
94
  get commands(): Engine["commands"];
63
95
  /** Interface items (toolbar buttons and the like) that the editor and its plugins offer. */
@@ -69,6 +101,7 @@ export declare class Editor {
69
101
  subscribe: Engine["subscribe"];
70
102
  select: Engine["select"];
71
103
  /** Adds a plugin. Returns a function that removes it. */
104
+ /** Adds a plugin, with its page-facing part if it has one. Returns what takes both away again. */
72
105
  use: Engine["use"];
73
106
  /** The document as HTML. `separator` goes between blocks (a newline makes it readable). */
74
107
  getHtml(separator?: string): string;
@@ -76,9 +109,19 @@ export declare class Editor {
76
109
  remoteImages(): string[];
77
110
  /** Sets the hint shown while the document is empty ("" for none). */
78
111
  setPlaceholder(text: string): void;
79
- /** Replaces the whole document and empties the history. */
112
+ /** Replaces the whole document and empties the history. Reported to nobody as a `change`, like setting the value of a text field. */
80
113
  setHtml(html: string): void;
114
+ /** Listens for `change`, `focus`, `blur` or `selection` (see `EditorEvents`). Returns a function that stops listening. */
115
+ on<K extends keyof EditorEvents>(event: K, listener: EditorEvents[K]): () => void;
116
+ /** Read-only on or off. While the editor is disabled it stays read-only whatever this says. */
81
117
  setReadOnly(value: boolean): void;
118
+ /** Disabled on or off. Disabled is read-only, not focusable and out of the tab order. */
119
+ setDisabled(value: boolean): void;
120
+ /**
121
+ * The page attributes that say what the editor allows: editable, read-only (focusable, `aria-readonly`) or disabled (not
122
+ * focusable, `aria-disabled`). Only the attributes this sets are touched; a `tabindex` the page gave is its own.
123
+ */
124
+ private applyAccess;
82
125
  /** Calls back after the document or selection changed, once per turn. Same as `subscribe`, kept for older callers. */
83
126
  onChange(listener: Listener): () => void;
84
127
  destroy(): void;
@@ -104,6 +147,8 @@ export declare class Editor {
104
147
  * Brings the editor's selection up to date from the page. Only while the editor has the focus: when focus is somewhere else (a dialog,
105
148
  * a toolbar button) the page's selection is not the editor's business, and the editor keeps the selection it had.
106
149
  */
150
+ /** Set while a selection is read when some point had to be moved to a text position. */
151
+ private fellBack;
107
152
  private readSelection;
108
153
  /**
109
154
  * A point on the page as a position in the document. Some browsers report a selection that spans whole blocks (select all, in Firefox)
@@ -124,6 +169,8 @@ export declare class Editor {
124
169
  */
125
170
  private deleteVisualLine;
126
171
  private onPaste;
172
+ /** What copy, cut and drag write: the model's own HTML and text, then whatever formats plugins add. */
173
+ private copyFormats;
127
174
  /** Copy and cut write the model's own HTML and text, not the page's markup. */
128
175
  private onCopy;
129
176
  private onDragStart;
@@ -135,5 +182,11 @@ export declare class Editor {
135
182
  private pasteFiles;
136
183
  /** Keys go through the key map: a chord bound to a command runs it, and the event is stopped if the command changed something. */
137
184
  private onKeyDown;
185
+ /**
186
+ * Backspace at the very start of the document, and Delete at the very end, change something here (a code block or heading becomes a
187
+ * paragraph, a rule goes) but there is nothing for the browser to delete, so WebKit sends no input event for them. They are handled
188
+ * from the key instead.
189
+ */
190
+ private keyAtDocumentEdge;
138
191
  }
139
192
  export {};