@atom-forge/brickee 0.0.2 → 0.0.3

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 (62) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README-AI.md +10 -7
  3. package/dist/Editor.svelte +691 -833
  4. package/dist/LinkPrompt.svelte +1 -14
  5. package/dist/Preview.svelte +27 -54
  6. package/dist/Preview.svelte.d.ts +2 -4
  7. package/dist/PropsBar.svelte +35 -219
  8. package/dist/code.js +1 -1
  9. package/dist/dom.d.ts +8 -0
  10. package/dist/dom.js +9 -0
  11. package/dist/editor/BlockMenu.svelte +102 -0
  12. package/dist/editor/BlockMenu.svelte.d.ts +32 -0
  13. package/dist/editor/FormatBar.svelte +41 -0
  14. package/dist/editor/FormatBar.svelte.d.ts +16 -0
  15. package/dist/editor/drag.svelte.d.ts +63 -0
  16. package/dist/editor/drag.svelte.js +95 -0
  17. package/dist/editor/history.d.ts +24 -0
  18. package/dist/editor/history.js +86 -0
  19. package/dist/editor/sweep.svelte.d.ts +21 -0
  20. package/dist/editor/sweep.svelte.js +56 -0
  21. package/dist/html.d.ts +12 -0
  22. package/dist/html.js +206 -0
  23. package/dist/i18n.js +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +3 -2
  26. package/dist/kit/Empty.svelte +4 -15
  27. package/dist/kit/Frame.svelte +7 -37
  28. package/dist/locales/hu.js +4 -1
  29. package/dist/markdown.d.ts +20 -4
  30. package/dist/markdown.js +380 -85
  31. package/dist/model.js +2 -3
  32. package/dist/plugins/Gap.svelte +5 -8
  33. package/dist/plugins/GapPreview.svelte +4 -6
  34. package/dist/plugins/Map.svelte +2 -17
  35. package/dist/plugins/MapPreview.svelte +8 -10
  36. package/dist/plugins/Table.svelte +233 -256
  37. package/dist/plugins/TablePreview.svelte +28 -28
  38. package/dist/plugins/YouTube.svelte +3 -31
  39. package/dist/plugins/YouTubePreview.svelte +45 -43
  40. package/dist/plugins/index.js +3 -1
  41. package/dist/types.d.ts +21 -3
  42. package/dist/types.js +8 -1
  43. package/dist/view.js +1 -1
  44. package/docs/en/bundled-plugins.md +1 -1
  45. package/docs/en/documents-and-markdown.md +20 -14
  46. package/docs/en/editor-and-preview.md +6 -7
  47. package/docs/en/getting-started.md +8 -0
  48. package/docs/en/index.md +1 -1
  49. package/docs/en/recipes-and-pitfalls.md +2 -2
  50. package/docs/en/server-side.md +0 -17
  51. package/docs/en/upgrading.md +14 -0
  52. package/docs/en/writing-plugins.md +36 -10
  53. package/docs/hu/bundled-plugins.md +1 -1
  54. package/docs/hu/documents-and-markdown.md +20 -14
  55. package/docs/hu/editor-and-preview.md +6 -7
  56. package/docs/hu/getting-started.md +8 -0
  57. package/docs/hu/index.md +1 -1
  58. package/docs/hu/recipes-and-pitfalls.md +2 -2
  59. package/docs/hu/server-side.md +0 -17
  60. package/docs/hu/upgrading.md +14 -0
  61. package/docs/hu/writing-plugins.md +36 -10
  62. package/package.json +9 -1
@@ -0,0 +1,41 @@
1
+ <!-- The formatting bar above a text selection: bold, italic… link, plain text. -->
2
+ <script lang="ts">
3
+ import { portal } from '../dom';
4
+ import type { Icon } from '../palette';
5
+
6
+ let {
7
+ x,
8
+ y,
9
+ tools,
10
+ label,
11
+ onpick
12
+ }: {
13
+ /** The middle of the selection's top edge; the bar sits centred above it. */
14
+ x: number;
15
+ y: number;
16
+ tools: { cmd: string; icon: Icon; title: string }[];
17
+ label: string;
18
+ onpick: (cmd: string) => void;
19
+ } = $props();
20
+ </script>
21
+
22
+ <!-- in the body (portal): placed against the viewport even inside a transformed parent -->
23
+ <div
24
+ use:portal
25
+ class="fixed z-20 flex -translate-x-1/2 translate-y-[calc(-100%-8px)] gap-0.5 rounded-[10px] border border-frame bg-surface p-1 shadow-[0_6px_24px_rgb(0_0_0/0.18)]"
26
+ style:left="{x}px"
27
+ style:top="{y}px"
28
+ role="toolbar"
29
+ aria-label={label}
30
+ >
31
+ {#each tools as t (t.cmd)}
32
+ <button
33
+ type="button"
34
+ class="grid h-[30px] min-w-[30px] cursor-pointer place-items-center rounded-md border-0 bg-transparent text-surface-contrast hover:bg-[rgb(120_120_140/0.14)]"
35
+ title={t.title}
36
+ aria-label={t.title}
37
+ onmousedown={(e) => e.preventDefault()}
38
+ onclick={() => onpick(t.cmd)}><t.icon size={16} /></button
39
+ >
40
+ {/each}
41
+ </div>
@@ -0,0 +1,16 @@
1
+ import type { Icon } from '../palette';
2
+ type $$ComponentProps = {
3
+ /** The middle of the selection's top edge; the bar sits centred above it. */
4
+ x: number;
5
+ y: number;
6
+ tools: {
7
+ cmd: string;
8
+ icon: Icon;
9
+ title: string;
10
+ }[];
11
+ label: string;
12
+ onpick: (cmd: string) => void;
13
+ };
14
+ declare const FormatBar: import("svelte").Component<$$ComponentProps, {}, "">;
15
+ type FormatBar = ReturnType<typeof FormatBar>;
16
+ export default FormatBar;
@@ -0,0 +1,63 @@
1
+ import type { StoredBlock } from '../types';
2
+ export type DragSource = {
3
+ /** The token of the editor the blocks come from. */
4
+ editor: string;
5
+ ids: string[];
6
+ /** The blocks in their stored form, for another editor to copy in. */
7
+ payload: StoredBlock[];
8
+ /** Their rows (a drop target inside one of them is inside the dragged blocks). */
9
+ rows: HTMLElement[];
10
+ /** Takes the blocks out of their editor (after another editor copied them in). */
11
+ remove: () => void;
12
+ };
13
+ export type Hub = {
14
+ src: DragSource | null;
15
+ target: {
16
+ editor: string;
17
+ over: string;
18
+ after: boolean;
19
+ } | null;
20
+ /** The editor worked in last. */
21
+ active: string | null;
22
+ };
23
+ /** The hub of this tree of editors: the parent editor's, or a new one for a top editor. Call in component init. */
24
+ export declare function useHub(): Hub;
25
+ export interface DragHost {
26
+ /** This editor's token (a string, not an object: $state would proxy an object and break identity checks). */
27
+ token: string;
28
+ hub: Hub;
29
+ root(): HTMLElement;
30
+ ownRow(node: Element | null): HTMLElement | null;
31
+ /** The blocks a drag from this block moves: the block selection if the block is in it, otherwise the block. */
32
+ moving(id: string): string[];
33
+ /** The stored form of these blocks. */
34
+ payload(ids: string[]): StoredBlock[];
35
+ /** Takes these blocks out (an undo step). */
36
+ remove(ids: string[]): void;
37
+ /** Can this editor hold these blocks (the plugins it offers)? */
38
+ holds(payload: StoredBlock[]): boolean;
39
+ /** Moves its own blocks before or after another one (an undo step). */
40
+ move(ids: string[], over: string, after: boolean): void;
41
+ /** Puts another editor's blocks before or after one of its own (an undo step). */
42
+ insert(payload: StoredBlock[], over: string, after: boolean): void;
43
+ /** Does a plugin this editor offers take files? */
44
+ takesFiles(): boolean;
45
+ /** Files dropped from outside the page, before or after one of its blocks. */
46
+ files(files: File[], over: string, after: boolean): void;
47
+ }
48
+ export declare function createDrag(host: DragHost): {
49
+ /** On a handle's dragstart. */
50
+ start(e: DragEvent, id: string): void;
51
+ /** On the editor's dragover. */
52
+ over(e: DragEvent): void;
53
+ /** On the editor's drop. */
54
+ drop(e: DragEvent): void;
55
+ /** On dragend: nothing is dragged any more. */
56
+ end(): void;
57
+ /** On the editor's dragleave: files dragged out of it leave no drop line behind (they have no dragend here). */
58
+ leave(e: DragEvent): void;
59
+ /** Is this block of this editor being dragged? */
60
+ moves: (id: string) => boolean;
61
+ /** The drop line before (or after) this block. */
62
+ mark: (id: string, after: boolean) => boolean;
63
+ };
@@ -0,0 +1,95 @@
1
+ // Dragging blocks by their handle, within an editor and between an editor and the editors nested in its blocks (and
2
+ // back). The editors of one tree share a hub through a context: what is being dragged, where it would land, and which
3
+ // editor was worked in last (only that one keeps a block selection).
4
+ import { getContext, setContext } from 'svelte';
5
+ const KEY = 'brickee:hub';
6
+ /** The hub of this tree of editors: the parent editor's, or a new one for a top editor. Call in component init. */
7
+ export function useHub() {
8
+ const own = $state({ src: null, target: null, active: null });
9
+ const hub = getContext(KEY) ?? own;
10
+ setContext(KEY, hub);
11
+ return hub;
12
+ }
13
+ export function createDrag(host) {
14
+ const { hub, token } = host;
15
+ /** Can the dragged blocks land in this editor? Not inside themselves, and only plugins this editor offers. */
16
+ function accepts(src) {
17
+ if (src.editor === token)
18
+ return true;
19
+ if (src.rows.some((r) => r.contains(host.root())))
20
+ return false;
21
+ return host.holds(src.payload);
22
+ }
23
+ return {
24
+ /** On a handle's dragstart. */
25
+ start(e, id) {
26
+ const ids = host.moving(id);
27
+ hub.src = {
28
+ editor: token,
29
+ ids,
30
+ payload: host.payload(ids),
31
+ rows: ids.map((x) => host.root().querySelector(`[data-block="${x}"]`)).filter(Boolean),
32
+ remove: () => host.remove(ids)
33
+ };
34
+ hub.target = null;
35
+ if (!e.dataTransfer)
36
+ return;
37
+ e.dataTransfer.effectAllowed = 'move';
38
+ e.dataTransfer.setData('text/plain', ' ');
39
+ const row = e.currentTarget.closest('.row');
40
+ if (row)
41
+ e.dataTransfer.setDragImage(row, 0, 0);
42
+ },
43
+ /** On the editor's dragover. */
44
+ over(e) {
45
+ // A nested editor deeper in the tree already took this event.
46
+ if (e.defaultPrevented)
47
+ return;
48
+ // Files from outside the page, when a plugin here takes files.
49
+ const files = !hub.src && !!e.dataTransfer?.types.includes('Files') && host.takesFiles();
50
+ if (!hub.src && !files)
51
+ return;
52
+ const row = host.ownRow(e.target);
53
+ if (!row || (hub.src && !accepts(hub.src)))
54
+ return;
55
+ e.preventDefault();
56
+ const r = row.getBoundingClientRect();
57
+ hub.target = { editor: token, over: row.dataset.block, after: e.clientY > r.top + r.height / 2 };
58
+ },
59
+ /** On the editor's drop. */
60
+ drop(e) {
61
+ const src = hub.src;
62
+ const at = hub.target;
63
+ if (at?.editor !== token)
64
+ return;
65
+ e.preventDefault();
66
+ hub.src = hub.target = null;
67
+ if (!src) {
68
+ const files = [...(e.dataTransfer?.files ?? [])];
69
+ if (files.length)
70
+ host.files(files, at.over, at.after);
71
+ return;
72
+ }
73
+ if (src.ids.includes(at.over))
74
+ return;
75
+ if (src.editor === token)
76
+ return host.move(src.ids, at.over, at.after);
77
+ // From another editor: copy in through the stored form, then remove at the source.
78
+ host.insert(src.payload, at.over, at.after);
79
+ src.remove();
80
+ },
81
+ /** On dragend: nothing is dragged any more. */
82
+ end() {
83
+ hub.src = hub.target = null;
84
+ },
85
+ /** On the editor's dragleave: files dragged out of it leave no drop line behind (they have no dragend here). */
86
+ leave(e) {
87
+ if (!hub.src && hub.target?.editor === token && !host.root().contains(e.relatedTarget))
88
+ hub.target = null;
89
+ },
90
+ /** Is this block of this editor being dragged? */
91
+ moves: (id) => hub.src?.editor === token && hub.src.ids.includes(id),
92
+ /** The drop line before (or after) this block. */
93
+ mark: (id, after) => hub.target?.editor === token && hub.target.over === id && hub.target.after === after && !hub.src?.ids.includes(id)
94
+ };
95
+ }
@@ -0,0 +1,24 @@
1
+ export declare class History<S> {
2
+ #private;
3
+ private take;
4
+ private apply;
5
+ private limit;
6
+ private settle;
7
+ /**
8
+ * @param take a snapshot of the editor now
9
+ * @param apply puts the editor back into a snapshot
10
+ * @param limit how many steps are kept
11
+ * @param settle how long (ms) typing waits before its step is closed
12
+ */
13
+ constructor(take: () => S, apply: (s: S) => void, limit?: number, settle?: number);
14
+ /** Call on every text edit: consecutive typing collapses into one undo step. */
15
+ typed(): void;
16
+ /** Wrap every structural change: pushes an undo step. */
17
+ mutate(fn: () => void): void;
18
+ /** Drops the undo and redo steps (a document loaded from outside). */
19
+ forget(): void;
20
+ undo(): void;
21
+ redo(): void;
22
+ /** Stops the pending timer (the editor is going away). */
23
+ dispose(): void;
24
+ }
@@ -0,0 +1,86 @@
1
+ // The editor's undo history: snapshots of the document (and where the caret was), one step per structural change,
2
+ // consecutive typing collapsed into one step.
3
+ export class History {
4
+ take;
5
+ apply;
6
+ limit;
7
+ settle;
8
+ #past = [];
9
+ #future = [];
10
+ #last;
11
+ #pending = false;
12
+ #timer;
13
+ /**
14
+ * @param take a snapshot of the editor now
15
+ * @param apply puts the editor back into a snapshot
16
+ * @param limit how many steps are kept
17
+ * @param settle how long (ms) typing waits before its step is closed
18
+ */
19
+ constructor(take, apply, limit = 300, settle = 600) {
20
+ this.take = take;
21
+ this.apply = apply;
22
+ this.limit = limit;
23
+ this.settle = settle;
24
+ this.#last = take();
25
+ }
26
+ #push(s) {
27
+ this.#past.push(s);
28
+ if (this.#past.length > this.limit)
29
+ this.#past.shift();
30
+ this.#future = [];
31
+ }
32
+ #settle() {
33
+ clearTimeout(this.#timer);
34
+ this.#pending = false;
35
+ }
36
+ /** Call on every text edit: consecutive typing collapses into one undo step. */
37
+ typed() {
38
+ if (!this.#pending) {
39
+ this.#push(this.#last);
40
+ this.#pending = true;
41
+ }
42
+ clearTimeout(this.#timer);
43
+ this.#timer = setTimeout(() => {
44
+ this.#pending = false;
45
+ this.#last = this.take();
46
+ }, this.settle);
47
+ }
48
+ /** Wrap every structural change: pushes an undo step. */
49
+ mutate(fn) {
50
+ this.#settle();
51
+ this.#push(this.take());
52
+ fn();
53
+ this.#last = this.take();
54
+ }
55
+ /** Drops the undo and redo steps (a document loaded from outside). */
56
+ forget() {
57
+ this.#settle();
58
+ this.#past = [];
59
+ this.#future = [];
60
+ this.#last = this.take();
61
+ }
62
+ undo() {
63
+ this.#settle();
64
+ const s = this.#past.pop();
65
+ if (!s)
66
+ return;
67
+ this.#future.push(this.take());
68
+ this.#restore(s);
69
+ }
70
+ redo() {
71
+ this.#settle();
72
+ const s = this.#future.pop();
73
+ if (!s)
74
+ return;
75
+ this.#past.push(this.take());
76
+ this.#restore(s);
77
+ }
78
+ #restore(s) {
79
+ this.#last = s;
80
+ this.apply(s);
81
+ }
82
+ /** Stops the pending timer (the editor is going away). */
83
+ dispose() {
84
+ clearTimeout(this.#timer);
85
+ }
86
+ }
@@ -0,0 +1,21 @@
1
+ export interface SweepHost {
2
+ /** The editor's root element. */
3
+ root(): HTMLElement;
4
+ /** The editor's own row (not a nested editor's) that holds this node, or null. */
5
+ ownRow(node: Element | null): HTMLElement | null;
6
+ readonly(): boolean;
7
+ /** Select the blocks from `anchor` to `head`. */
8
+ select(anchor: string, head: string): void;
9
+ /** Drop the block selection (back to text selection). */
10
+ clear(): void;
11
+ /** Take the focus from the text field, to the root (the block selection's keys). */
12
+ focusRoot(): void;
13
+ }
14
+ export declare function createSweep(host: SweepHost): {
15
+ /** True while whole blocks are being swept (the browser must not extend its own selection meanwhile). */
16
+ readonly sweeping: boolean;
17
+ /** On the editor's mousedown: a press in a block's text may start a sweep. */
18
+ start(e: MouseEvent): void;
19
+ /** Stops a sweep and its listeners (also when the editor goes away). */
20
+ end: () => void;
21
+ };
@@ -0,0 +1,56 @@
1
+ // A text selection dragged out of its block turns into a selection of whole blocks (each block is its own editable
2
+ // field, so the browser cannot select text across them). Nested editors run the same logic, so a sweep that leaves a
3
+ // box is taken over by the editor around it, with the box as one block.
4
+ export function createSweep(host) {
5
+ let sweeping = $state(false);
6
+ let from = null;
7
+ function move(e) {
8
+ const start = host.ownRow(from);
9
+ const here = document.elementFromPoint(e.clientX, e.clientY);
10
+ if (!start || !here)
11
+ return;
12
+ const root = host.root();
13
+ const row = root.contains(here) ? host.ownRow(here) : null;
14
+ // Left this editor, or came back to the starting block: back to plain text selection.
15
+ if (!root.contains(here) || row === start) {
16
+ if (sweeping) {
17
+ sweeping = false;
18
+ host.clear();
19
+ }
20
+ return;
21
+ }
22
+ if (!row)
23
+ return;
24
+ if (!sweeping) {
25
+ sweeping = true;
26
+ host.focusRoot();
27
+ }
28
+ getSelection()?.removeAllRanges();
29
+ host.select(start.dataset.block, row.dataset.block);
30
+ }
31
+ function end() {
32
+ removeEventListener('mousemove', move);
33
+ removeEventListener('mouseup', end);
34
+ from = null;
35
+ sweeping = false;
36
+ }
37
+ return {
38
+ /** True while whole blocks are being swept (the browser must not extend its own selection meanwhile). */
39
+ get sweeping() {
40
+ return sweeping;
41
+ },
42
+ /** On the editor's mousedown: a press in a block's text may start a sweep. */
43
+ start(e) {
44
+ if (host.readonly() || e.button !== 0 || e.shiftKey)
45
+ return;
46
+ const t = e.target;
47
+ if (!t.closest('.ce') || t.closest('.grab'))
48
+ return;
49
+ from = t;
50
+ addEventListener('mousemove', move);
51
+ addEventListener('mouseup', end, { once: true });
52
+ },
53
+ /** Stops a sweep and its listeners (also when the editor goes away). */
54
+ end
55
+ };
56
+ }
package/dist/html.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { Block, BlockPlugin } from './types';
2
+ /** HTML (a pasted fragment or a page) as markdown. Browser only. */
3
+ export declare function markdownFromHtml(html: string): string;
4
+ /**
5
+ * HTML -> blocks, the way the editor takes a formatted paste: headings, paragraphs, lists (nested, numbered with their
6
+ * start, to-dos from checkboxes), quotes, code (`pre`, with a `language-…` class), rules, and inline marks — also
7
+ * Google Docs' styled spans. A table goes to the first of `plugins` that takes tables (`fromTable`). Images become
8
+ * links; scripts, styles and form controls are dropped. Browser only.
9
+ */
10
+ export declare function fromHtml(html: string, opts?: {
11
+ plugins?: BlockPlugin[];
12
+ }): Block[];
package/dist/html.js ADDED
@@ -0,0 +1,206 @@
1
+ // Formatted paste: HTML from a web page, Google Docs, Word… -> markdown -> blocks. Going through markdown reuses
2
+ // everything fromMarkdown does (list nesting, tables for a plugin that takes them, safe links); the HTML is only read,
3
+ // never inserted. Browser only (DOMParser).
4
+ import { codeSpan, escLine, escText, fromMarkdown, mdUrl } from './markdown';
5
+ /** Never read: no text of theirs belongs in a document. */
6
+ const SKIP = new Set(['SCRIPT', 'STYLE', 'HEAD', 'TITLE', 'META', 'LINK', 'TEMPLATE', 'NOSCRIPT', 'SVG', 'CANVAS', 'IFRAME', 'OBJECT', 'EMBED', 'BUTTON', 'SELECT', 'TEXTAREA', 'INPUT', 'VIDEO', 'AUDIO']);
7
+ /** Elements that make blocks; everything else is inline. */
8
+ const BLOCK = new Set([
9
+ 'ADDRESS', 'ARTICLE', 'ASIDE', 'BLOCKQUOTE', 'BODY', 'CENTER', 'DD', 'DETAILS', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE', 'FOOTER', 'FORM',
10
+ 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', 'HR', 'HTML', 'LI', 'MAIN', 'NAV', 'OL', 'P', 'PRE', 'SECTION', 'SUMMARY', 'TABLE', 'UL'
11
+ ]);
12
+ const BLOCK_SELECTOR = [...BLOCK].map((t) => t.toLowerCase()).join(',');
13
+ /** A block, or an inline element that holds blocks (Google Docs wraps a whole copy in a <b>): a container then. */
14
+ const isBlock = (n) => n instanceof HTMLElement && (BLOCK.has(n.tagName) || (!SKIP.has(n.tagName) && !!n.querySelector(BLOCK_SELECTOR)));
15
+ /** Marks around text, with its spaces kept outside (`** bold**` is no mark). */
16
+ function wrap(inner, open, close = open) {
17
+ const [, a, core, b] = /^(\s*)([\s\S]*?)(\s*)$/.exec(inner);
18
+ return core ? a + open + core + close + b : inner;
19
+ }
20
+ /** The marks a span's inline style stands for (Google Docs writes bold and italic as styles). */
21
+ function styled(el, inner) {
22
+ const s = el.style;
23
+ const weight = s.fontWeight;
24
+ let out = inner;
25
+ if (weight === 'bold' || weight === 'bolder' || Number(weight) >= 600)
26
+ out = wrap(out, '**');
27
+ if (s.fontStyle === 'italic')
28
+ out = wrap(out, '*');
29
+ if (s.textDecoration.includes('line-through') || s.textDecorationLine.includes('line-through'))
30
+ out = wrap(out, '~~');
31
+ if (s.textDecoration.includes('underline') || s.textDecorationLine.includes('underline'))
32
+ out = wrap(out, '<u>', '</u>');
33
+ if (s.verticalAlign === 'super')
34
+ out = wrap(out, '<sup>', '</sup>');
35
+ if (s.verticalAlign === 'sub')
36
+ out = wrap(out, '<sub>', '</sub>');
37
+ return out;
38
+ }
39
+ /** Inline content as inline markdown; `\n` where the HTML breaks a line. */
40
+ function inline(nodes) {
41
+ let text = '';
42
+ // HTML collapses neighbouring spaces, also across marks ("a <b> b</b>").
43
+ const out = {
44
+ add(s) {
45
+ text += /[ \n]$/.test(text) || !text ? s.replace(/^ +/, '') : s;
46
+ }
47
+ };
48
+ for (const n of nodes) {
49
+ if (n.nodeType === Node.TEXT_NODE) {
50
+ out.add(escText((n.textContent ?? '').replace(/[ \t\r\n]+/g, ' ')));
51
+ continue;
52
+ }
53
+ if (!(n instanceof HTMLElement) || SKIP.has(n.tagName))
54
+ continue;
55
+ const tag = n.tagName;
56
+ if (tag === 'BR') {
57
+ out.add('\n');
58
+ continue;
59
+ }
60
+ if (tag === 'IMG') {
61
+ // No images in the package: a picture becomes a link to it.
62
+ const src = n.getAttribute('src') ?? '';
63
+ if (/^https?:/i.test(src))
64
+ out.add(`[${escText(n.getAttribute('alt') || src)}](${mdUrl(src)})`);
65
+ continue;
66
+ }
67
+ if (tag === 'CODE' || tag === 'KBD' || tag === 'SAMP' || tag === 'TT') {
68
+ const text = (n.textContent ?? '').replace(/\s+/g, ' ');
69
+ out.add(text.trim() ? codeSpan(text) : text);
70
+ continue;
71
+ }
72
+ // A block inside inline content (a paragraph in a list item): its own line.
73
+ const inner = isBlock(n) ? `\n${inline(n.childNodes).trim()}\n` : inline(n.childNodes);
74
+ if (tag === 'A') {
75
+ const href = n.getAttribute('href') ?? '';
76
+ out.add(inner.trim() && href && !href.startsWith('#') ? wrap(inner, '[', `](${mdUrl(href)})`) : inner);
77
+ }
78
+ else if (tag === 'B' || tag === 'STRONG') {
79
+ // Google Docs wraps a whole copy in <b style="font-weight:normal">.
80
+ const w = n.style.fontWeight;
81
+ out.add(w === 'normal' || w === '400' ? styled(n, inner) : wrap(inner, '**'));
82
+ }
83
+ else if (tag === 'I' || tag === 'EM' || tag === 'CITE' || tag === 'DFN' || tag === 'VAR')
84
+ out.add(wrap(inner, '*'));
85
+ else if (tag === 'U' || tag === 'INS')
86
+ out.add(wrap(inner, '<u>', '</u>'));
87
+ else if (tag === 'S' || tag === 'DEL' || tag === 'STRIKE')
88
+ out.add(wrap(inner, '~~'));
89
+ else if (tag === 'SUB' || tag === 'SUP')
90
+ out.add(wrap(inner, `<${tag.toLowerCase()}>`, `</${tag.toLowerCase()}>`));
91
+ else
92
+ out.add(styled(n, inner));
93
+ }
94
+ return text;
95
+ }
96
+ /** Inline markdown as the lines of a paragraph: each line escaped where it would read as a block start. */
97
+ const lines = (md) => md
98
+ .split('\n')
99
+ .map((l) => l.trim())
100
+ .filter(Boolean)
101
+ .map(escLine);
102
+ /** A container's children as markdown chunks (blocks); runs of inline content become paragraphs. */
103
+ function chunks(el) {
104
+ const out = [];
105
+ let run = [];
106
+ const flush = () => {
107
+ const ls = lines(inline(run));
108
+ if (ls.length)
109
+ out.push(ls.join('\n'));
110
+ run = [];
111
+ };
112
+ for (const n of el.childNodes) {
113
+ if (n instanceof HTMLElement && SKIP.has(n.tagName))
114
+ continue;
115
+ if (isBlock(n)) {
116
+ flush();
117
+ out.push(...block(n));
118
+ }
119
+ else
120
+ run.push(n);
121
+ }
122
+ flush();
123
+ return out;
124
+ }
125
+ function block(el) {
126
+ const tag = el.tagName;
127
+ if (/^H[1-6]$/.test(tag)) {
128
+ const text = inline(el.childNodes).replace(/\s*\n\s*/g, ' ').trim();
129
+ return text ? [`${'#'.repeat(+tag[1])} ${text}`] : [];
130
+ }
131
+ if (tag === 'HR')
132
+ return ['---'];
133
+ if (tag === 'UL' || tag === 'OL') {
134
+ const md = list(el, 0);
135
+ return md ? [md] : [];
136
+ }
137
+ if (tag === 'PRE') {
138
+ const text = (el.textContent ?? '').replace(/\r/g, '').replace(/\n$/, '');
139
+ const lang = /(?:^|\s)(?:language|lang)-([\w+#.-]+)/.exec(`${el.className} ${el.querySelector('code')?.className ?? ''}`)?.[1] ?? '';
140
+ const fence = '`'.repeat(Math.max(3, ...(text.match(/`+/g) ?? []).map((r) => r.length + 1)));
141
+ return [`${fence}${lang}\n${text}\n${fence}`];
142
+ }
143
+ if (tag === 'BLOCKQUOTE') {
144
+ const inner = chunks(el);
145
+ return inner.length ? [inner.map((c) => c.split('\n').map((l) => `> ${l}`).join('\n')).join('\n>\n')] : [];
146
+ }
147
+ if (tag === 'TABLE')
148
+ return table(el);
149
+ return chunks(el);
150
+ }
151
+ /** A list and its nested lists, `indent` spaces in. */
152
+ function list(el, indent) {
153
+ const pad = ' '.repeat(indent);
154
+ const ordered = el.tagName === 'OL';
155
+ let n = Number(el.getAttribute('start')) || 1;
156
+ const out = [];
157
+ for (const li of el.children) {
158
+ if (li.tagName !== 'LI')
159
+ continue;
160
+ const box = li.querySelector(':scope > input[type="checkbox"], :scope > p > input[type="checkbox"], :scope > label > input[type="checkbox"]');
161
+ const marker = ordered ? `${n++}. ` : box ? `- [${box.checked || box.hasAttribute('checked') ? 'x' : ' '}] ` : '- ';
162
+ const own = [...li.childNodes].filter((c) => !(c instanceof HTMLElement && (c.tagName === 'UL' || c.tagName === 'OL')));
163
+ const [first = '', ...more] = lines(inline(own));
164
+ out.push(pad + marker + first);
165
+ const inner = ' '.repeat(indent + marker.length);
166
+ for (const l of more)
167
+ out.push(inner + l);
168
+ for (const sub of li.children)
169
+ if (sub.tagName === 'UL' || sub.tagName === 'OL') {
170
+ const md = list(sub, indent + marker.length);
171
+ if (md)
172
+ out.push(md);
173
+ }
174
+ }
175
+ return out.join('\n');
176
+ }
177
+ /** A table as a GFM table: the first row is the header; the alignment from the header cells. */
178
+ function table(el) {
179
+ const rows = [...el.querySelectorAll('tr')].filter((tr) => tr.closest('table') === el);
180
+ const grid = rows.map((tr) => [...tr.children]
181
+ .filter((c) => c.tagName === 'TD' || c.tagName === 'TH')
182
+ .map((c) => inline(c.childNodes).replace(/\s*\n\s*/g, '<br>').trim().replace(/\|/g, '\\|')));
183
+ if (!grid.length || !grid[0].length)
184
+ return [];
185
+ const width = Math.max(...grid.map((r) => r.length));
186
+ const align = [...rows[0].children].map((c) => {
187
+ const a = (c.style.textAlign || c.getAttribute('align') || '').toLowerCase();
188
+ return a === 'center' ? ':-:' : a === 'right' ? '--:' : '---';
189
+ });
190
+ const row = (r) => `| ${[...r, ...Array(width - r.length).fill('')].join(' | ')} |`;
191
+ return [[row(grid[0]), row(Array.from({ length: width }, (_, i) => align[i] ?? '---')), ...grid.slice(1).map(row)].join('\n')];
192
+ }
193
+ /** HTML (a pasted fragment or a page) as markdown. Browser only. */
194
+ export function markdownFromHtml(html) {
195
+ const doc = new DOMParser().parseFromString(html, 'text/html');
196
+ return chunks(doc.body).join('\n\n');
197
+ }
198
+ /**
199
+ * HTML -> blocks, the way the editor takes a formatted paste: headings, paragraphs, lists (nested, numbered with their
200
+ * start, to-dos from checkboxes), quotes, code (`pre`, with a `language-…` class), rules, and inline marks — also
201
+ * Google Docs' styled spans. A table goes to the first of `plugins` that takes tables (`fromTable`). Images become
202
+ * links; scripts, styles and form controls are dropped. Browser only.
203
+ */
204
+ export function fromHtml(html, opts = {}) {
205
+ return fromMarkdown(markdownFromHtml(html), opts);
206
+ }
package/dist/i18n.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // UI text. The editor and the plugins are written in English; a host passes `messages` (English text -> its own) to
2
2
  // the top editor or preview, and nested editors and plugin views inherit them. `{name}` placeholders are filled in.
3
3
  import { getContext, setContext } from 'svelte';
4
- const KEY = 'block-editor:text';
4
+ const KEY = 'brickee:text';
5
5
  export const translator = (messages) => (text, vars) => {
6
6
  const out = messages?.[text] ?? text;
7
7
  return vars ? out.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m)) : out;
package/dist/index.d.ts CHANGED
@@ -5,9 +5,10 @@ export { default as Empty } from './kit/Empty.svelte';
5
5
  export * from './kit/util';
6
6
  export * from './model';
7
7
  export { inline, fromStored, toStored, fromMarkdown, toMarkdown } from './markdown';
8
+ export { fromHtml } from './html';
8
9
  export { provideView, useView } from './view';
9
10
  export { provideCodeLanguages, useCodeLanguages, type CodeLanguage } from './code';
10
11
  export { scrollSync, scrollFraction, setScrollFraction, type ScrollSync } from './scroll';
11
12
  export { provideText, useText, translator, type Messages, type Translate } from './i18n';
12
13
  export { PALETTE_ITEMS, BUILT_IN_IDS, narrowPalette, type Icon, type PaletteItem } from './palette';
13
- export type { MarkdownContext, Block, BlockType, Content, PluginPreviewProps, PluginViewProps, PropField, Props, BlockPlugin } from './types';
14
+ export type { MarkdownContext, MarkdownTable, Block, BlockType, Content, PluginPreviewProps, PluginViewProps, PropField, Props, BlockPlugin } from './types';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- // block-editor: a Svelte 5 block editor. The core is markdown (`md:p`, `md:h2`, `md:code`…) plus the kit to write plugins
1
+ // Brickee: a Svelte 5 block editor. The core is markdown (`md:p`, `md:h2`, `md:code`…) plus the kit to write plugins
2
2
  // with; every other block is a plugin with an editor view, a site preview and optionally a markdown form. The bundled
3
- // plugins (block-editor/plugins) are plugins like any other. The stored form is `StoredBlock[]` (see model.ts).
3
+ // plugins (@atom-forge/brickee/plugins) are plugins like any other. The stored form is `StoredBlock[]` (see model.ts).
4
4
  export { default as Editor } from './Editor.svelte';
5
5
  export { default as Preview } from './Preview.svelte';
6
6
  // The kit: what plugins are written with (box frame, empty state, helpers).
@@ -9,6 +9,7 @@ export { default as Empty } from './kit/Empty.svelte';
9
9
  export * from './kit/util';
10
10
  export * from './model';
11
11
  export { inline, fromStored, toStored, fromMarkdown, toMarkdown } from './markdown';
12
+ export { fromHtml } from './html';
12
13
  export { provideView, useView } from './view';
13
14
  export { provideCodeLanguages, useCodeLanguages } from './code';
14
15
  export { scrollSync, scrollFraction, setScrollFraction } from './scroll';