@atom-forge/brickee 0.0.0-stage → 0.0.1

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 (87) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README-AI.md +283 -0
  4. package/README.md +41 -2
  5. package/dist/Editor.svelte +1968 -0
  6. package/dist/Editor.svelte.d.ts +22 -0
  7. package/dist/ImagePicker.svelte +131 -0
  8. package/dist/ImagePicker.svelte.d.ts +13 -0
  9. package/dist/LinkPrompt.svelte +42 -0
  10. package/dist/LinkPrompt.svelte.d.ts +8 -0
  11. package/dist/PluginHost.svelte +41 -0
  12. package/dist/PluginHost.svelte.d.ts +13 -0
  13. package/dist/Preview.svelte +186 -0
  14. package/dist/Preview.svelte.d.ts +35 -0
  15. package/dist/PropsBar.svelte +395 -0
  16. package/dist/PropsBar.svelte.d.ts +20 -0
  17. package/dist/dom.d.ts +29 -0
  18. package/dist/dom.js +224 -0
  19. package/dist/i18n.d.ts +7 -0
  20. package/dist/i18n.js +18 -0
  21. package/dist/index.d.ts +13 -0
  22. package/dist/index.js +16 -0
  23. package/dist/kit/Empty.svelte +24 -0
  24. package/dist/kit/Empty.svelte.d.ts +8 -0
  25. package/dist/kit/Frame.svelte +76 -0
  26. package/dist/kit/Frame.svelte.d.ts +15 -0
  27. package/dist/kit/util.d.ts +13 -0
  28. package/dist/kit/util.js +28 -0
  29. package/dist/locales/hu.d.ts +2 -0
  30. package/dist/locales/hu.js +147 -0
  31. package/dist/markdown.d.ts +25 -0
  32. package/dist/markdown.js +219 -0
  33. package/dist/media.d.ts +17 -0
  34. package/dist/media.js +12 -0
  35. package/dist/model.d.ts +21 -0
  36. package/dist/model.js +45 -0
  37. package/dist/palette.d.ts +19 -0
  38. package/dist/palette.js +23 -0
  39. package/dist/plugins/Gap.svelte +28 -0
  40. package/dist/plugins/Gap.svelte.d.ts +15 -0
  41. package/dist/plugins/GapPreview.svelte +17 -0
  42. package/dist/plugins/GapPreview.svelte.d.ts +5 -0
  43. package/dist/plugins/Map.svelte +47 -0
  44. package/dist/plugins/Map.svelte.d.ts +12 -0
  45. package/dist/plugins/MapPreview.svelte +26 -0
  46. package/dist/plugins/MapPreview.svelte.d.ts +5 -0
  47. package/dist/plugins/Table.svelte +563 -0
  48. package/dist/plugins/Table.svelte.d.ts +28 -0
  49. package/dist/plugins/TablePreview.svelte +62 -0
  50. package/dist/plugins/TablePreview.svelte.d.ts +5 -0
  51. package/dist/plugins/YouTube.svelte +52 -0
  52. package/dist/plugins/YouTube.svelte.d.ts +8 -0
  53. package/dist/plugins/YouTubePreview.svelte +76 -0
  54. package/dist/plugins/YouTubePreview.svelte.d.ts +5 -0
  55. package/dist/plugins/index.d.ts +20 -0
  56. package/dist/plugins/index.js +94 -0
  57. package/dist/plugins/util.d.ts +3 -0
  58. package/dist/plugins/util.js +8 -0
  59. package/dist/schema.d.ts +11 -0
  60. package/dist/schema.js +25 -0
  61. package/dist/scroll.d.ts +18 -0
  62. package/dist/scroll.js +57 -0
  63. package/dist/types.d.ts +133 -0
  64. package/dist/types.js +43 -0
  65. package/dist/view.d.ts +6 -0
  66. package/dist/view.js +11 -0
  67. package/docs/en/bundled-plugins.md +99 -0
  68. package/docs/en/documents-and-markdown.md +115 -0
  69. package/docs/en/editor-and-preview.md +141 -0
  70. package/docs/en/getting-started.md +81 -0
  71. package/docs/en/host-contexts.md +275 -0
  72. package/docs/en/index.md +38 -0
  73. package/docs/en/recipes-and-pitfalls.md +122 -0
  74. package/docs/en/server-side.md +95 -0
  75. package/docs/en/upgrading.md +55 -0
  76. package/docs/en/writing-plugins.md +305 -0
  77. package/docs/hu/bundled-plugins.md +99 -0
  78. package/docs/hu/documents-and-markdown.md +115 -0
  79. package/docs/hu/editor-and-preview.md +141 -0
  80. package/docs/hu/getting-started.md +81 -0
  81. package/docs/hu/host-contexts.md +275 -0
  82. package/docs/hu/index.md +38 -0
  83. package/docs/hu/recipes-and-pitfalls.md +122 -0
  84. package/docs/hu/server-side.md +95 -0
  85. package/docs/hu/upgrading.md +55 -0
  86. package/docs/hu/writing-plugins.md +305 -0
  87. package/package.json +110 -5
@@ -0,0 +1,13 @@
1
+ export { default as Editor } from './Editor.svelte';
2
+ export { default as Preview } from './Preview.svelte';
3
+ export { default as Frame } from './kit/Frame.svelte';
4
+ export { default as Empty } from './kit/Empty.svelte';
5
+ export * from './kit/util';
6
+ export * from './model';
7
+ export { inline, fromStored, toStored, fromMarkdown, toMarkdown } from './markdown';
8
+ export { provideMedia, useMedia, findMedia, type MediaItem } from './media';
9
+ export { provideView, useView } from './view';
10
+ export { scrollSync, scrollFraction, setScrollFraction, type ScrollSync } from './scroll';
11
+ export { provideText, useText, translator, type Messages, type Translate } from './i18n';
12
+ 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';
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ // block-editor: a Svelte 5 block editor. The core is markdown (`md:p`, `md:h2`, `md:code`…) plus the kit to write plugins
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).
4
+ export { default as Editor } from './Editor.svelte';
5
+ export { default as Preview } from './Preview.svelte';
6
+ // The kit: what plugins are written with (box frame, empty state, helpers).
7
+ export { default as Frame } from './kit/Frame.svelte';
8
+ export { default as Empty } from './kit/Empty.svelte';
9
+ export * from './kit/util';
10
+ export * from './model';
11
+ export { inline, fromStored, toStored, fromMarkdown, toMarkdown } from './markdown';
12
+ export { provideMedia, useMedia, findMedia } from './media';
13
+ export { provideView, useView } from './view';
14
+ export { scrollSync, scrollFraction, setScrollFraction } from './scroll';
15
+ export { provideText, useText, translator } from './i18n';
16
+ export { PALETTE_ITEMS, BUILT_IN_IDS, narrowPalette } from './palette';
@@ -0,0 +1,24 @@
1
+ <!-- What a plugin without content shows until its props are set. -->
2
+ <script lang="ts">
3
+ import { useText } from '../i18n';
4
+ import type { Icon } from '../palette';
5
+
6
+ let { icon: I, text }: { icon: Icon; text: string } = $props();
7
+ const tr = useText();
8
+ </script>
9
+
10
+ <div class="empty"><I size={18} /> {tr(text)}</div>
11
+
12
+ <style>
13
+ .empty {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ gap: 8px;
18
+ min-height: 96px;
19
+ border: 1px dashed #d4d4da;
20
+ border-radius: 10px;
21
+ color: #a0a0a8;
22
+ font: 14px system-ui, sans-serif;
23
+ }
24
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { Icon } from '../palette';
2
+ type $$ComponentProps = {
3
+ icon: Icon;
4
+ text: string;
5
+ };
6
+ declare const Empty: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type Empty = ReturnType<typeof Empty>;
8
+ export default Empty;
@@ -0,0 +1,76 @@
1
+ <!-- The editor look of a plugin with nested blocks: a thin frame with the block's fields on its top (or, read-only, a
2
+ heading line), and the nested editor. -->
3
+ <script lang="ts">
4
+ import { useText } from '../i18n';
5
+ import Editor from '../Editor.svelte';
6
+ import { claimFields } from '../PluginHost.svelte';
7
+ import type { BlockPlugin, StoredBlock } from '../types';
8
+
9
+ interface Props {
10
+ blocks: StoredBlock[];
11
+ readonly: boolean;
12
+ palette: string[];
13
+ plugins: BlockPlugin[];
14
+ /** Summary of the block's props (title…); `empty` is shown when there is none. */
15
+ heading?: string;
16
+ empty?: string;
17
+ image?: string;
18
+ placeholder?: string;
19
+ }
20
+ let {
21
+ blocks = $bindable(),
22
+ readonly,
23
+ palette,
24
+ plugins,
25
+ heading = '',
26
+ empty = '',
27
+ image,
28
+ placeholder
29
+ }: Props = $props();
30
+ const tr = useText();
31
+ const fields = claimFields();
32
+ </script>
33
+
34
+ <div class="frame">
35
+ {#if fields}
36
+ <div class="fields">{@render fields()}</div>
37
+ {:else if heading || empty || image}
38
+ <div class="head">
39
+ {#if image}<img src={image} alt="" draggable="false" />{/if}
40
+ <span class:muted={!heading}>{heading || empty}</span>
41
+ </div>
42
+ {/if}
43
+ <Editor bind:value={blocks} {readonly} {palette} {plugins} nested placeholder={placeholder ?? tr('Write inside…')} />
44
+ </div>
45
+
46
+ <style>
47
+ .frame {
48
+ position: relative;
49
+ border: 1px solid #e5e5ea;
50
+ border-radius: 10px;
51
+ /* left: room for the inner blocks' handle bar (22px left of the row, which reaches 8px out) */
52
+ padding: 0.8em 1.1em 0.4em calc(30px + 0.5em);
53
+ }
54
+ /* the fields reach to the frame's edges, the frame's padding taken back */
55
+ .fields {
56
+ margin: calc(-0.8em + 4px) calc(-1.1em + 4px) 0.5em calc(-30px - 0.5em + 4px);
57
+ }
58
+ .head {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 8px;
62
+ margin: 0 9em 0.3em 0;
63
+ font: 600 13px/1.4 system-ui, sans-serif;
64
+ color: #1d1d1f;
65
+ }
66
+ .head img {
67
+ height: 28px;
68
+ width: 44px;
69
+ object-fit: cover;
70
+ border-radius: 4px;
71
+ }
72
+ .muted {
73
+ font-weight: 400;
74
+ color: #a0a0a8;
75
+ }
76
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { BlockPlugin, StoredBlock } from '../types';
2
+ interface Props {
3
+ blocks: StoredBlock[];
4
+ readonly: boolean;
5
+ palette: string[];
6
+ plugins: BlockPlugin[];
7
+ /** Summary of the block's props (title…); `empty` is shown when there is none. */
8
+ heading?: string;
9
+ empty?: string;
10
+ image?: string;
11
+ placeholder?: string;
12
+ }
13
+ declare const Frame: import("svelte").Component<Props, {}, "blocks">;
14
+ type Frame = ReturnType<typeof Frame>;
15
+ export default Frame;
@@ -0,0 +1,13 @@
1
+ /** A #hex colour, or the fallback. */
2
+ export declare const safeColor: (c: unknown, fallback: string) => string;
3
+ export declare const lines: (s: string) => string[];
4
+ /** The base value of a per-view prop (see PropField.views): the value itself, or the first view's. */
5
+ export declare const baseValue: (v: unknown) => unknown;
6
+ /** A per-view prop's value for each of these views (the first is the base); a view without its own takes the one before. */
7
+ export declare function viewValues(v: unknown, keys: string[]): unknown[];
8
+ /** Plain text made safe inside markdown (no inline marks, no block start). */
9
+ export declare const mdText: (s: unknown) => string;
10
+ /** Markdown lines quoted (`> `), for a box rendered as a blockquote. */
11
+ export declare const mdQuote: (md: string) => string;
12
+ /** Markdown parts joined as separate blocks, empty ones left out. */
13
+ export declare const mdJoin: (...parts: (string | false | null | undefined)[]) => string;
@@ -0,0 +1,28 @@
1
+ // Helpers for writing plugins: values come from the block fields, so the previews only use safe ones.
2
+ /** A #hex colour, or the fallback. */
3
+ export const safeColor = (c, fallback) => (/^#[0-9a-f]{3,8}$/i.test(String(c ?? '')) ? String(c) : fallback);
4
+ export const lines = (s) => s
5
+ .split('\n')
6
+ .map((l) => l.trim())
7
+ .filter(Boolean);
8
+ const byView = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
9
+ /** The base value of a per-view prop (see PropField.views): the value itself, or the first view's. */
10
+ export const baseValue = (v) => (byView(v) ? Object.values(v)[0] : v);
11
+ /** A per-view prop's value for each of these views (the first is the base); a view without its own takes the one before. */
12
+ export function viewValues(v, keys) {
13
+ const own = byView(v) ? v : { [keys[0]]: v };
14
+ let last;
15
+ return keys.map((k) => (last = own[k] ?? last));
16
+ }
17
+ /** Plain text made safe inside markdown (no inline marks, no block start). */
18
+ export const mdText = (s) => String(s ?? '')
19
+ .replace(/[\\`*~[\]<_#]/g, '\\$&')
20
+ .replace(/\n+/g, ' ')
21
+ .trim();
22
+ /** Markdown lines quoted (`> `), for a box rendered as a blockquote. */
23
+ export const mdQuote = (md) => md
24
+ .split('\n')
25
+ .map((l) => (l ? `> ${l}` : '>'))
26
+ .join('\n');
27
+ /** Markdown parts joined as separate blocks, empty ones left out. */
28
+ export const mdJoin = (...parts) => parts.filter(Boolean).join('\n\n');
@@ -0,0 +1,2 @@
1
+ import type { Messages } from '../i18n';
2
+ export declare const hu: Messages;
@@ -0,0 +1,147 @@
1
+ export const hu = {
2
+ // szerkesztő
3
+ "Write something, or press '/' for blocks…": 'Írj valamit, vagy nyomj „/”-t a blokkokért…',
4
+ 'Add a block below': 'Új blokk alá',
5
+ 'Drag to move, click to select': 'Húzd az áthelyezéshez, kattints a kijelöléshez',
6
+ Done: 'Kész',
7
+ inherited: 'örökölt',
8
+ Settings: 'Beállítások',
9
+ 'Hide settings': 'Beállítások elrejtése',
10
+ None: 'Nincs',
11
+ Images: 'Képek',
12
+ Choose: 'Választás',
13
+ 'Choose…': 'Válassz…',
14
+ '{n} picked': '{n} kiválasztva',
15
+ language: 'nyelv',
16
+ 'No images attached to this page yet.': 'Még nincs kép csatolva.',
17
+ 'New block at the end': 'Új blokk a végére',
18
+ Formatting: 'Formázás',
19
+ 'Turn into': 'Átalakítás',
20
+ 'Block actions': 'Blokkműveletek',
21
+ 'Block type': 'Blokktípus',
22
+ Delete: 'Törlés',
23
+ '{n} blocks': '{n} blokk',
24
+ 'Remove this block': 'A blokk törlése',
25
+ Unwrap: 'Kicsomagolás',
26
+ 'Move the inner blocks out': 'A belső blokkok kikerülnek',
27
+ 'Put inside': 'Bele',
28
+ 'Put {n} blocks inside': '{n} blokk bele',
29
+ 'Loses formatting': 'A formázás elvész',
30
+ OK: 'OK',
31
+ Remove: 'Eltávolítás',
32
+ 'Bold (Ctrl+B)': 'Félkövér (Ctrl+B)',
33
+ 'Italic (Ctrl+I)': 'Dőlt (Ctrl+I)',
34
+ 'Underline (Ctrl+U)': 'Aláhúzott (Ctrl+U)',
35
+ 'Strikethrough (Ctrl+Shift+X)': 'Áthúzott (Ctrl+Shift+X)',
36
+ 'Subscript (NO₂)': 'Alsó index (NO₂)',
37
+ 'Superscript (m²)': 'Felső index (m²)',
38
+ 'Code (Ctrl+E)': 'Kód (Ctrl+E)',
39
+ 'Link (Ctrl+K)': 'Link (Ctrl+K)',
40
+ 'Plain text: remove the formatting (Ctrl+\\)': 'Sima szöveg: a formázás törlése (Ctrl+\\)',
41
+ 'List item': 'Listaelem',
42
+ Language: 'Nyelv',
43
+ 'e.g. ts, py, css': 'pl. ts, py, css',
44
+ code: 'kód',
45
+ // beépített blokkok
46
+ Text: 'Szöveg',
47
+ 'Plain paragraph': 'Sima bekezdés',
48
+ 'text paragraph': 'szöveg bekezdés',
49
+ 'Heading 1': 'Címsor 1',
50
+ 'Heading 2': 'Címsor 2',
51
+ 'Heading 3': 'Címsor 3',
52
+ 'Heading 4': 'Címsor 4',
53
+ 'Heading 5': 'Címsor 5',
54
+ 'Heading 6': 'Címsor 6',
55
+ 'Large heading': 'Nagy cím',
56
+ 'Medium heading': 'Közepes cím',
57
+ 'Small heading': 'Kis cím',
58
+ 'Sub-heading': 'Alcím',
59
+ 'Minor heading': 'Kisebb alcím',
60
+ 'Smallest heading': 'Legkisebb alcím',
61
+ 'heading h1 title': 'cím címsor',
62
+ 'heading h2 title': 'cím címsor alcím',
63
+ 'heading h3 title': 'cím címsor alcím',
64
+ 'heading h4 title': 'alcím címsor',
65
+ 'heading h5 title': 'alcím címsor',
66
+ 'heading h6 title': 'alcím címsor',
67
+ 'Bulleted list': 'Felsorolás',
68
+ 'Simple bullets': 'Pontokba szedve',
69
+ 'bullet list ul': 'lista felsorolás pont',
70
+ 'Numbered list': 'Számozott lista',
71
+ 'Ordered items': 'Sorszámozott elemek',
72
+ 'numbered ordered list ol': 'számozott lista sorszám',
73
+ 'To-do': 'Teendő',
74
+ 'Checkable item': 'Kipipálható elem',
75
+ 'todo task checkbox': 'teendő feladat pipa',
76
+ Quote: 'Idézet',
77
+ 'Highlighted quote': 'Kiemelt idézet',
78
+ 'quote blockquote': 'idézet',
79
+ Code: 'Kód',
80
+ 'Monospaced block': 'Fix szélességű betűkkel',
81
+ 'code snippet pre': 'kód forráskód',
82
+ Divider: 'Elválasztó',
83
+ 'Horizontal line': 'Vízszintes vonal',
84
+ 'divider hr line separator': 'elválasztó vonal',
85
+ // bővítmények: név, leírás, kulcsszavak
86
+ YouTube: 'YouTube',
87
+ 'Video from a link': 'Videó linkből',
88
+ 'youtube video embed': 'youtube videó beágyazás',
89
+ Table: 'Táblázat',
90
+ 'Rows and columns': 'Sorok és oszlopok',
91
+ 'table grid rows columns price list': 'táblázat sor oszlop árlista',
92
+ Map: 'Térkép',
93
+ Gap: 'Térköz',
94
+ 'Empty space': 'Üres hely',
95
+ 'gap space spacer whitespace': 'térköz hely üres távolság',
96
+ 'Google Maps for a place': 'Google Térkép egy helyről',
97
+ 'map google location address place': 'térkép hely cím',
98
+ // mezők
99
+ Title: 'Cím',
100
+ Height: 'Magasság',
101
+ '1 line': '1 sor',
102
+ '{n} lines': '{n} sor',
103
+ Original: 'Eredeti',
104
+ 'Video link or id': 'A videó linkje vagy azonosítója',
105
+ 'Place or address': 'Hely vagy cím',
106
+ 'e.g. Pécs, Széchenyi tér': 'pl. Pécs, Széchenyi tér',
107
+ Zoom: 'Nagyítás',
108
+ // a blokkok címkéi (a blokk tetején, a mezők előtt)
109
+ left: 'bal',
110
+ right: 'jobb',
111
+ danger: 'vigyázat',
112
+ image: 'kép',
113
+ youtube: 'youtube',
114
+ table: 'táblázat',
115
+ map: 'térkép',
116
+ gap: 'térköz',
117
+ // a bővítmények nézetei
118
+ 'Write inside…': 'Írj bele…',
119
+ 'Paste a YouTube link above': 'Illeszd be fent a YouTube-linket',
120
+ 'Set a place or an address above': 'Adj meg fent egy helyet vagy címet',
121
+ Row: 'Sor',
122
+ Column: 'Oszlop',
123
+ 'Insert a row above': 'Új sor fölé',
124
+ 'Insert a row below': 'Új sor alá',
125
+ 'Delete this row': 'A sor törlése',
126
+ 'Insert a column left': 'Új oszlop balra',
127
+ 'Insert a column right': 'Új oszlop jobbra',
128
+ 'Delete this column': 'Az oszlop törlése',
129
+ 'Align column left': 'Az oszlop balra zárva',
130
+ 'Center column': 'Az oszlop középre zárva',
131
+ 'Align column right': 'Az oszlop jobbra zárva',
132
+ 'Header row': 'Fejléc (első sor)',
133
+ 'Footer row (e.g. sums)': 'Lábléc (utolsó sor, pl. összesítő)',
134
+ 'Drag to move the row': 'Húzd a sor áthelyezéséhez',
135
+ 'Drag to move the column': 'Húzd az oszlop áthelyezéséhez',
136
+ 'Enter: next row · Shift+Enter: line break · paste from a spreadsheet': 'Enter: következő sor · Shift+Enter: sortörés · táblázatból be is illeszthető',
137
+ Heading: 'Fejléc',
138
+ 'Row {r}, column {c}': '{r}. sor, {c}. oszlop',
139
+ 'Add a column': 'Új oszlop',
140
+ 'Add a row': 'Új sor',
141
+ // előnézet
142
+ 'For supporters only': 'Csak támogatóknak',
143
+ 'YouTube video': 'YouTube-videó',
144
+ video: 'videó',
145
+ 'Play {title}': 'Lejátszás: {title}',
146
+ 'Map: {place}': 'Térkép: {place}'
147
+ };
@@ -0,0 +1,25 @@
1
+ import type { MediaItem } from './media';
2
+ import { type Block, type BlockPlugin, type StoredBlock } from './types';
3
+ /** Inline markdown -> inline html (b, i, u, sub, sup, strike, code, a, br). Backslash escapes are honoured; links are
4
+ * http(s), mailto, tel or site-relative only. */
5
+ export declare function inline(md: string): string;
6
+ /** Inline html -> inline markdown (browser only). `code` keeps the text verbatim, as in a code block. */
7
+ export declare function htmlToMarkdown(html: string, code?: boolean): string;
8
+ /** One block -> its markdown snippet, without indentation. `n` is the ordinal of a numbered item. */
9
+ export declare function blockToMd(b: Block, n?: number): string;
10
+ /** One markdown snippet -> block fields. */
11
+ export declare function mdToBlock(md: string): Pick<Block, 'type' | 'html' | 'checked' | 'props'>;
12
+ /** Inline html -> the same letters without formatting (line breaks kept), as a code block holds them. */
13
+ export declare function plainHtml(html: string): string;
14
+ /** Editor block -> stored block: `md:<kind>` with inline markdown text, or the plugin's own type. */
15
+ export declare function toStored(b: Block): StoredBlock;
16
+ /** Stored block -> editor block with a fresh id. Unknown types (md: or plugin) are kept as they are. */
17
+ export declare function fromStored(s: StoredBlock): Block;
18
+ /** Markdown text -> blocks. Supports headings, lists, todos, quotes, fenced code, hr and basic inline marks. */
19
+ export declare function fromMarkdown(src: string): Block[];
20
+ /** Blocks -> markdown text (browser only: uses the DOM to read the inline html). A plugin block is written by its
21
+ * plugin's `markdown`; one without it is left out. */
22
+ export declare function toMarkdown(blocks: Block[], opts?: {
23
+ plugins?: BlockPlugin[];
24
+ media?: MediaItem[];
25
+ }): string;
@@ -0,0 +1,219 @@
1
+ import { MAX_INDENT, MD, newBlock, numbering } from './types';
2
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
3
+ /** Characters escaped in plain text so they never turn into inline markup. */
4
+ const SPECIAL = /[\\`*~[\]<]/g;
5
+ const MARK = '\u0000';
6
+ /** Inline markdown -> inline html (b, i, u, sub, sup, strike, code, a, br). Backslash escapes are honoured; links are
7
+ * http(s), mailto, tel or site-relative only. */
8
+ export function inline(md) {
9
+ const kept = [];
10
+ const guarded = md.replace(/\\([\\`*~[\]<>#+.!_-])/g, (_, c) => `${MARK}${kept.push(c) - 1}${MARK}`);
11
+ const restore = (s, raw) => s.replace(/\u0000(\d+)\u0000/g, (_, n) => (raw ? '\\' : '') + esc(kept[+n]));
12
+ return guarded
13
+ .split(/(`[^`]+`)/)
14
+ .map((seg, i) => {
15
+ if (i % 2)
16
+ return `<code>${restore(esc(seg.slice(1, -1)), true)}</code>`;
17
+ const html = esc(seg)
18
+ .replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
19
+ .replace(/(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)/g, '<i>$1</i>')
20
+ .replace(/~~(.+?)~~/g, '<strike>$1</strike>')
21
+ .replace(/&lt;(u|sub|sup)&gt;(.+?)&lt;\/\1&gt;/g, '<$1>$2</$1>')
22
+ .replace(/\[(.+?)\]\(((?:https?:|mailto:|tel:|\/(?!\/))[^)\s]+)\)/g, '<a href="$2">$1</a>')
23
+ .replace(/\n/g, '<br>');
24
+ return restore(html, false);
25
+ })
26
+ .join('');
27
+ }
28
+ function walk(n, code) {
29
+ if (n.nodeType === Node.TEXT_NODE) {
30
+ // Zero-width spaces only hold the caret outside a mark (see plainText()).
31
+ const t = (n.textContent ?? '').replace(/\u200B/g, '');
32
+ return code ? t : t.replace(SPECIAL, '\\$&');
33
+ }
34
+ if (!(n instanceof HTMLElement))
35
+ return '';
36
+ const inCode = code || n.tagName === 'CODE';
37
+ const inner = [...n.childNodes].map((c) => walk(c, inCode)).join('');
38
+ switch (n.tagName) {
39
+ case 'B':
40
+ case 'STRONG':
41
+ return inner ? `**${inner}**` : '';
42
+ case 'I':
43
+ case 'EM':
44
+ return inner ? `*${inner}*` : '';
45
+ case 'U':
46
+ case 'SUB':
47
+ case 'SUP': {
48
+ const tag = n.tagName.toLowerCase();
49
+ return inner ? `<${tag}>${inner}</${tag}>` : '';
50
+ }
51
+ case 'S':
52
+ case 'STRIKE':
53
+ case 'DEL':
54
+ return inner ? `~~${inner}~~` : '';
55
+ case 'CODE':
56
+ return inner && !code ? `\`${inner}\`` : inner;
57
+ case 'A':
58
+ return `[${inner}](${n.getAttribute('href') ?? ''})`;
59
+ case 'BR':
60
+ return '\n';
61
+ default:
62
+ return inner;
63
+ }
64
+ }
65
+ /** Inline html -> inline markdown (browser only). `code` keeps the text verbatim, as in a code block. */
66
+ export function htmlToMarkdown(html, code = false) {
67
+ const d = document.createElement('div');
68
+ d.innerHTML = html;
69
+ return walk(d, code);
70
+ }
71
+ /** Line starts that markdown would read as a block; a paragraph starting like this gets a leading backslash. */
72
+ const BLOCK_START = /^(?:#{1,6} |[-*+] |\d{1,3}[.)] |>|```|([-*_])\1{2,}\s*$)/;
73
+ const RULE = /^([-*_])\1{2,}\s*$/;
74
+ /** Opening code fence with an optional language: ```ts, ```c++, ```objective-c… */
75
+ const FENCE = /^```([\w+#.-]*)[^\S\n]*/;
76
+ const LINE = /^(?:(#{1,6}) |[-*+] \[([ xX])\] |([-*+]) |(\d{1,3})[.)] |(>) ?)?([\s\S]*)$/;
77
+ /** One block -> its markdown snippet, without indentation. `n` is the ordinal of a numbered item. */
78
+ export function blockToMd(b, n = 1) {
79
+ if (b.type === 'plugin')
80
+ return '';
81
+ if (b.type === 'hr')
82
+ return '---';
83
+ if (b.type === 'code')
84
+ return '```' + (b.props?.lang ?? '') + '\n' + htmlToMarkdown(b.html, true) + '\n```';
85
+ const t = htmlToMarkdown(b.html);
86
+ switch (b.type) {
87
+ case 'h1':
88
+ case 'h2':
89
+ case 'h3':
90
+ case 'h4':
91
+ case 'h5':
92
+ case 'h6':
93
+ return `${'#'.repeat(+b.type[1])} ${t}`;
94
+ case 'ul':
95
+ return `- ${t}`;
96
+ case 'ol':
97
+ return `${n}. ${t}`;
98
+ case 'todo':
99
+ return `- [${b.checked ? 'x' : ' '}] ${t}`;
100
+ case 'quote':
101
+ return `> ${t}`;
102
+ default:
103
+ return BLOCK_START.test(t) ? '\\' + t : t;
104
+ }
105
+ }
106
+ /** One markdown snippet -> block fields. */
107
+ export function mdToBlock(md) {
108
+ const fence = FENCE.exec(md);
109
+ if (fence) {
110
+ const body = md.slice(fence[0].length).replace(/^\n/, '').replace(/\n?```\s*$/, '');
111
+ return { type: 'code', html: codeHtml(body), ...(fence[1] ? { props: { lang: fence[1] } } : {}) };
112
+ }
113
+ if (RULE.test(md))
114
+ return { type: 'hr', html: '' };
115
+ const m = LINE.exec(md);
116
+ const type = m[1]
117
+ ? `h${m[1].length}`
118
+ : m[2]
119
+ ? 'todo'
120
+ : m[3]
121
+ ? 'ul'
122
+ : m[4]
123
+ ? 'ol'
124
+ : m[5]
125
+ ? 'quote'
126
+ : 'p';
127
+ let text = m[6];
128
+ // Undo the backslash blockToMd put before a block-like start (text escapes such as `\*` stay).
129
+ if (type === 'p' && text[0] === '\\' && !/[\\`*~[\]<]/.test(text[1] ?? '\\') && BLOCK_START.test(text.slice(1)))
130
+ text = text.slice(1);
131
+ return { type, html: inline(text), ...(type === 'todo' ? { checked: m[2].toLowerCase() === 'x' } : {}) };
132
+ }
133
+ const BUILT_IN = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'todo', 'quote', 'code', 'hr'];
134
+ const codeHtml = (text) => esc(text).replace(/\n/g, '<br>');
135
+ /** Inline html -> the same letters without formatting (line breaks kept), as a code block holds them. */
136
+ export function plainHtml(html) {
137
+ const d = document.createElement('div');
138
+ d.innerHTML = html.replace(/<br\s*\/?>/gi, '\n');
139
+ return codeHtml(d.textContent ?? '');
140
+ }
141
+ /** Editor block -> stored block: `md:<kind>` with inline markdown text, or the plugin's own type. */
142
+ export function toStored(b) {
143
+ if (b.type === 'plugin')
144
+ return { type: b.plugin, indent: b.indent, props: b.props ?? {}, ...(b.blocks ? { blocks: b.blocks } : {}) };
145
+ const out = { type: MD + b.type, indent: b.indent };
146
+ if (b.type === 'code')
147
+ out.text = htmlToMarkdown(b.html, true);
148
+ else if (b.type !== 'hr')
149
+ out.text = htmlToMarkdown(b.html);
150
+ if (b.type === 'todo')
151
+ out.props = { checked: !!b.checked };
152
+ if (b.type === 'code' && b.props?.lang)
153
+ out.props = { lang: b.props.lang };
154
+ return out;
155
+ }
156
+ /** Stored block -> editor block with a fresh id. Unknown types (md: or plugin) are kept as they are. */
157
+ export function fromStored(s) {
158
+ const kind = s.type.slice(MD.length);
159
+ if (!s.type.startsWith(MD) || !BUILT_IN.includes(kind))
160
+ return newBlock('plugin', '', { plugin: s.type, indent: s.indent, props: s.props ?? {}, blocks: s.blocks });
161
+ const text = s.text ?? '';
162
+ const html = kind === 'code' ? codeHtml(text) : kind === 'hr' ? '' : inline(text);
163
+ return newBlock(kind, html, {
164
+ indent: s.indent,
165
+ ...(kind === 'todo' ? { checked: !!s.props?.checked } : {}),
166
+ ...(kind === 'code' && s.props?.lang ? { props: { lang: s.props.lang } } : {})
167
+ });
168
+ }
169
+ /** Markdown text -> blocks. Supports headings, lists, todos, quotes, fenced code, hr and basic inline marks. */
170
+ export function fromMarkdown(src) {
171
+ const out = [];
172
+ const lines = src.replace(/\r/g, '').split('\n');
173
+ for (let i = 0; i < lines.length; i++) {
174
+ const line = lines[i];
175
+ const fence = FENCE.exec(line.trimStart());
176
+ if (fence) {
177
+ const code = [];
178
+ i++;
179
+ while (i < lines.length && !/^\s*```/.test(lines[i]))
180
+ code.push(lines[i++]);
181
+ out.push(newBlock('code', codeHtml(code.join('\n')), fence[1] ? { props: { lang: fence[1] } } : {}));
182
+ continue;
183
+ }
184
+ if (!line.trim())
185
+ continue;
186
+ const lead = /^\s*/.exec(line)[0];
187
+ const indent = Math.min(MAX_INDENT, Math.floor(lead.replace(/\t/g, ' ').length / 2));
188
+ const { type, html, ...extra } = mdToBlock(line.slice(lead.length));
189
+ out.push(newBlock(type, html, { indent, ...extra }));
190
+ }
191
+ return out;
192
+ }
193
+ /** Blocks -> markdown text (browser only: uses the DOM to read the inline html). A plugin block is written by its
194
+ * plugin's `markdown`; one without it is left out. */
195
+ export function toMarkdown(blocks, opts = {}) {
196
+ const nums = numbering(blocks);
197
+ const listy = (t) => t === 'ul' || t === 'ol' || t === 'todo';
198
+ const md = (b) => {
199
+ if (b.type !== 'plugin')
200
+ return (' ').repeat(listy(b.type) || b.type === 'p' ? b.indent : 0) + blockToMd(b, nums[b.id]);
201
+ const plugin = opts.plugins?.find((p) => p.type === b.plugin);
202
+ if (!plugin?.markdown)
203
+ return '';
204
+ const inner = b.blocks?.length ? toMarkdown(b.blocks.map(fromStored), opts) : '';
205
+ return plugin.markdown(b.props ?? {}, { inner, media: opts.media ?? [] }).trim();
206
+ };
207
+ let out = '';
208
+ let prev;
209
+ for (const b of blocks) {
210
+ const text = md(b);
211
+ if (!text)
212
+ continue;
213
+ if (prev)
214
+ out += listy(b.type) && listy(prev.type) ? '\n' : '\n\n';
215
+ out += text;
216
+ prev = b;
217
+ }
218
+ return out;
219
+ }
@@ -0,0 +1,17 @@
1
+ export interface MediaItem {
2
+ /** Stored in the block's props. */
3
+ id: string;
4
+ /** File name or title, shown in the picker. */
5
+ name: string;
6
+ /** Address of the image (a thumbnail size is fine for the editor). */
7
+ url: string;
8
+ /** Where it comes from (e.g. the attachment category), shown as a heading in the picker. */
9
+ group?: string;
10
+ }
11
+ type Get = () => MediaItem[];
12
+ /** Call in a component's init. Without its own list, a nested editor or preview keeps its parent's. */
13
+ export declare function provideMedia(own: () => MediaItem[] | undefined): void;
14
+ /** Call in a component's init; the returned function reads the current list. */
15
+ export declare const useMedia: () => Get;
16
+ export declare const findMedia: (list: MediaItem[], id: unknown) => MediaItem | undefined;
17
+ export {};
package/dist/media.js ADDED
@@ -0,0 +1,12 @@
1
+ // The images a document can use: the host's attachments (on a CMS page, the page's uploaded images). Blocks store
2
+ // an attachment's id; the editor, the preview and the plugins look it up here.
3
+ import { getContext, setContext } from 'svelte';
4
+ const KEY = 'block-editor:media';
5
+ /** Call in a component's init. Without its own list, a nested editor or preview keeps its parent's. */
6
+ export function provideMedia(own) {
7
+ const parent = getContext(KEY);
8
+ setContext(KEY, () => own() ?? parent?.() ?? []);
9
+ }
10
+ /** Call in a component's init; the returned function reads the current list. */
11
+ export const useMedia = () => getContext(KEY) ?? (() => []);
12
+ export const findMedia = (list, id) => list.find((m) => m.id === id);
@@ -0,0 +1,21 @@
1
+ export interface StoredBlock {
2
+ type: string;
3
+ indent: number;
4
+ text?: string;
5
+ props?: Record<string, unknown>;
6
+ blocks?: StoredBlock[];
7
+ }
8
+ export declare const MD = "md:";
9
+ export declare const MAX_INDENT = 6;
10
+ /** Plugins with nested blocks hold one level only, so a document is at most this deep. */
11
+ export declare const MAX_DEPTH = 2;
12
+ export declare const isMd: (b: StoredBlock) => boolean;
13
+ /** Inline markdown -> plain text: marks, escapes and link targets dropped. */
14
+ export declare function inlineToPlainText(md: string): string;
15
+ /** Every text of a document, in order: for search, leads and meta descriptions. */
16
+ export declare function blocksToPlainText(blocks: StoredBlock[]): string;
17
+ /**
18
+ * Does a container's `accepts` list take this palette id? The list holds palette ids, and 'md:*' for every built-in
19
+ * block (text, headings, lists, quote, code, divider). No list: everything.
20
+ */
21
+ export declare const acceptsId: (accepts: readonly string[] | undefined, id: string) => boolean;