@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,395 @@
1
+ <!-- The fields of a plugin block, on the top of its box. Closed: only a small header (the block's tag); a click on it
2
+ opens the fields in place: a wrapping row (label and control side by side), text areas on their own full-width rows. An image field shows the picked image(s); a click
3
+ opens the picker (ImagePicker) with all the document's images. -->
4
+ <script lang="ts">
5
+ import { ColorPicker, Input, NativeSelect, Slider, Textarea, getPopupManager } from '@atom-forge/ui';
6
+ import ChevronDown from '@lucide/svelte/icons/chevron-down';
7
+ import { slide } from 'svelte/transition';
8
+ import ImagePicker from './ImagePicker.svelte';
9
+ import { useView } from './view';
10
+ import type { Translate } from './i18n';
11
+ import type { MediaItem } from './media';
12
+ import type { Block, PropField } from './types';
13
+
14
+ interface Props {
15
+ block: Block;
16
+ tag: string;
17
+ fields: PropField[];
18
+ media: MediaItem[];
19
+ tr: Translate;
20
+ /** Sets one property from its field's text and returns the text as it is now kept. */
21
+ setProp: (b: Block, f: PropField, raw: string) => string;
22
+ /** Sets one property to a value as it is ('' or undefined removes it). */
23
+ setValue: (b: Block, key: string, v: unknown) => void;
24
+ pickMedia: (b: Block, f: PropField, id: string) => void;
25
+ /** Start with the fields open (a block just made). */
26
+ open?: boolean;
27
+ }
28
+ let { block, tag, fields, media, tr, setProp, setValue, pickMedia, open: startOpen = false }: Props = $props();
29
+ // svelte-ignore state_referenced_locally
30
+ let open = $state(startOpen);
31
+
32
+ const text = (f: PropField) => {
33
+ const v = block.props?.[f.key];
34
+ return f.format ? f.format(v) : v == null ? '' : String(v);
35
+ };
36
+ const picked = (f: PropField): string[] => {
37
+ const v = block.props?.[f.key];
38
+ return Array.isArray(v) ? v : v ? [String(v)] : [];
39
+ };
40
+ const optionLabel = (f: PropField, i: number) => {
41
+ const opt = f.options?.[i];
42
+ return opt ? tr(opt.label, { n: opt.value }) : '';
43
+ };
44
+ // Sliders: with `min`/`max` a free number (left of `min` a "none" notch that removes the value), otherwise the
45
+ // index of one of the `options` (an unknown value: the first). With `views`, the slider edits the value of the view
46
+ // the host gives (useView; none given: the base): a view without its own shows the one it takes (from the views
47
+ // before it), and its notch means "take it".
48
+ const free = (f: PropField) => f.min != null && f.max != null;
49
+ const low = (f: PropField) => (f.none ? f.min! - Math.round((f.max! - f.min!) / 12) : f.min!);
50
+ const view = useView();
51
+ const viewOf = (f: PropField) => {
52
+ const v = view();
53
+ return f.views?.some((x) => x.key === v) ? v! : (f.views?.[0]?.key ?? '');
54
+ };
55
+ /** The view a per-view field edits, for its label (only when the host gives one). */
56
+ const viewLabel = (f: PropField) => {
57
+ const v = f.views && view() ? f.views.find((x) => x.key === viewOf(f)) : undefined;
58
+ return v ? tr(v.label) : '';
59
+ };
60
+ const byView = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v);
61
+ /** A view's own value (as text), '' when it has none. */
62
+ const own = (f: PropField, view: string) => {
63
+ const v = block.props?.[f.key];
64
+ const raw = byView(v) ? v[view] : view === f.views?.[0]?.key ? v : undefined;
65
+ return f.format ? f.format(raw) : raw == null ? '' : String(raw);
66
+ };
67
+ /** The text the picked view shows: its own, or the one it takes; and whether it is its own. */
68
+ const current = (f: PropField): { text: string; mine: boolean } => {
69
+ if (!f.views) return { text: text(f), mine: true };
70
+ const keys = f.views.map((v) => v.key);
71
+ const at = keys.indexOf(viewOf(f));
72
+ for (let i = at; i >= 0; i--) {
73
+ const t = own(f, keys[i]);
74
+ if (t) return { text: t, mine: i === at };
75
+ }
76
+ return { text: '', mine: at === 0 };
77
+ };
78
+ const pos = (f: PropField) => {
79
+ const t = current(f).text;
80
+ if (!free(f)) return Math.max(0, (f.options ?? []).findIndex((o) => o.value === t));
81
+ const n = Number(t);
82
+ return t && Number.isFinite(n) ? Math.min(f.max!, Math.max(f.min!, n)) : low(f);
83
+ };
84
+ const setPos = (f: PropField, v: number) => {
85
+ const t = free(f) ? (v < f.min! ? '' : String(Math.round(v))) : (f.options?.[v]?.value ?? '');
86
+ if (!f.views) return setProp(block, f, t);
87
+ // the views' own values, the picked one changed; only the base: stored alone
88
+ const keys = f.views.map((x) => x.key);
89
+ const next: Record<string, string> = {};
90
+ for (const k of keys) {
91
+ const value = k === viewOf(f) ? t : own(f, k);
92
+ if (value) next[k] = value;
93
+ }
94
+ const set = Object.keys(next);
95
+ setValue(block, f.key, !set.length ? '' : set.length === 1 && set[0] === keys[0] ? next[keys[0]] : next);
96
+ };
97
+ const posLabel = (f: PropField) => {
98
+ const { text: t, mine } = current(f);
99
+ const n = Number(t);
100
+ const shown = !free(f) ? optionLabel(f, pos(f)) : t && Number.isFinite(n) ? `${Math.round(n)}${f.unit ?? ''}` : tr(f.none ?? '');
101
+ return mine ? shown : `${shown} (${tr('inherited')})`;
102
+ };
103
+
104
+ // A text area keeps its text as typed while it has the focus (a list keeps its empty lines while typing); the block
105
+ // gets the parsed value. Out of focus it shows the value as kept (so undo shows through).
106
+ let editing = $state<{ key: string; text: string } | null>(null);
107
+
108
+ const short = $derived(fields.filter((f) => f.kind !== 'textarea'));
109
+ const long = $derived(fields.filter((f) => f.kind === 'textarea'));
110
+
111
+ const popups = getPopupManager();
112
+ const urlOf = (id: string) => media.find((m) => m.id === id);
113
+ function openPicker(e: MouseEvent, f: PropField) {
114
+ popups.open.component(
115
+ ImagePicker,
116
+ { media, multiple: f.kind === 'images', getPicked: () => picked(f), pick: (id: string) => pickMedia(block, f, id), tr },
117
+ { anchor: e, align: 'left' }
118
+ );
119
+ }
120
+ </script>
121
+
122
+ <div class="light bar" class:open>
123
+ <button type="button" class="head" aria-expanded={open} title={tr(open ? 'Hide settings' : 'Settings')} onmousedown={(e) => e.preventDefault()} onclick={() => (open = !open)}>
124
+ <span class="tag">{tag}</span>
125
+ <ChevronDown size={13} class="chev" />
126
+ </button>
127
+ {#if open}
128
+ <div class="body" transition:slide={{ duration: 180 }}>
129
+ <div class="grid">
130
+ {#each short as f (f.key)}
131
+ {#if f.kind === 'image' || f.kind === 'images'}
132
+ {@const ids = picked(f)}
133
+ <div class="field">
134
+ <span class="name">{tr(f.label)}</span>
135
+ <button type="button" class="chosen" title={tr('Choose')} onclick={(e) => openPicker(e, f)}>
136
+ {#each ids as id, n (id)}
137
+ {@const m = urlOf(id)}
138
+ {#if m}<img src={m.url} alt={m.name} />{:else}<span class="missing" title={id}>?</span>{/if}
139
+ {#if f.kind === 'images' && n === 5 && ids.length > 6}<span class="more">+{ids.length - 6}</span>{/if}
140
+ {:else}
141
+ <span class="empty">{tr('Choose…')}</span>
142
+ {/each}
143
+ </button>
144
+ </div>
145
+ {:else}
146
+ <!-- not a label around buttons (a slider's view buttons, a colour's clear): a click on a label goes to its first button -->
147
+ <svelte:element this={f.kind === 'slider' || f.kind === 'color' ? 'div' : 'label'} class="field">
148
+ <span class="name"
149
+ >{tr(f.label)}{#if viewLabel(f)}<span class="view" class:own={current(f).mine}>{viewLabel(f)}</span>{/if}</span
150
+ >
151
+ {#if f.kind === 'select'}
152
+ <NativeSelect
153
+ options={(f.options ?? []).map((opt) => ({ value: opt.value, label: tr(opt.label, { n: opt.value }) }))}
154
+ bind:value={() => text(f), (v: string) => setProp(block, f, v)}
155
+ />
156
+ {:else if f.kind === 'slider'}
157
+ <span class="line">
158
+ <span class="slider">
159
+ <Slider
160
+ compact
161
+ drawStops={!free(f) && (f.options?.length ?? 0) <= 10}
162
+ min={free(f) ? low(f) : 0}
163
+ max={free(f) ? f.max : (f.options?.length ?? 1) - 1}
164
+ step={free(f) ? (f.step ?? 1) : 1}
165
+ bind:value={() => pos(f), (v: number) => setPos(f, v)}
166
+ />
167
+ </span>
168
+ <span class="value">{posLabel(f)}</span>
169
+ </span>
170
+ {:else if f.kind === 'color'}
171
+ <!-- with `none`: an unset colour shows as "none" (a click sets one); a set one can be cleared -->
172
+ <span class="line">
173
+ {#if f.none && !text(f)}
174
+ <button type="button" class="nocolor" onclick={() => setProp(block, f, f.placeholder || '#000000')}>
175
+ <span class="swatch" aria-hidden="true"></span>{tr(f.none)}
176
+ </button>
177
+ {:else}
178
+ <ColorPicker bind:value={() => text(f) || '#000000', (v: string) => setProp(block, f, v)} />
179
+ {#if f.none}
180
+ <button type="button" class="clear" title={tr(f.none)} aria-label={tr(f.none)} onclick={() => setProp(block, f, '')}>×</button>
181
+ {/if}
182
+ {/if}
183
+ </span>
184
+ {:else}
185
+ <!-- the field shows the value as kept (a YouTube link becomes its id) -->
186
+ <Input monospace placeholder={f.placeholder && tr(f.placeholder)} bind:value={() => text(f), (v: string) => setProp(block, f, v)} />
187
+ {/if}
188
+ </svelte:element>
189
+ {/if}
190
+ {/each}
191
+ {#each long as f (f.key)}
192
+ <label class="field long" onfocusin={() => (editing = { key: f.key, text: text(f) })} onfocusout={() => (editing = null)}>
193
+ <span class="name">{tr(f.label)}</span>
194
+ <Textarea
195
+ rows={3}
196
+ placeholder={f.placeholder && tr(f.placeholder)}
197
+ bind:value={() => (editing?.key === f.key ? editing.text : text(f)), (v: string) => {
198
+ if (editing?.key === f.key) editing.text = v;
199
+ setProp(block, f, v);
200
+ }}
201
+ />
202
+ </label>
203
+ {/each}
204
+ </div>
205
+ </div>
206
+ {/if}
207
+ </div>
208
+
209
+ <style>
210
+ .bar {
211
+ display: grid;
212
+ justify-items: start;
213
+ gap: 6px;
214
+ margin-bottom: 4px;
215
+ border-radius: 8px;
216
+ font: 12px/1.4 system-ui, sans-serif;
217
+ color: #1d1d1f;
218
+ }
219
+ .bar.open {
220
+ justify-items: stretch;
221
+ margin-bottom: 8px;
222
+ padding: 8px 12px 12px;
223
+ background: rgba(120, 120, 140, 0.07);
224
+ }
225
+ .bar {
226
+ transition:
227
+ padding 0.18s,
228
+ background 0.18s;
229
+ }
230
+ /* the fields in 1–3 columns (as wide as the box allows), each with its label above */
231
+ .grid {
232
+ display: grid;
233
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
234
+ gap: 12px 16px;
235
+ padding-top: 8px;
236
+ }
237
+ .head {
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 2px;
241
+ padding: 0;
242
+ border: 0;
243
+ background: none;
244
+ color: #8e8e96;
245
+ cursor: pointer;
246
+ justify-self: start;
247
+ }
248
+ .head:hover .tag {
249
+ background: rgba(120, 120, 140, 0.2);
250
+ color: #1d1d1f;
251
+ }
252
+ .head :global(.chev) {
253
+ transition: rotate 0.15s;
254
+ }
255
+ .open .head :global(.chev) {
256
+ rotate: 180deg;
257
+ }
258
+ .tag {
259
+ padding: 1px 6px;
260
+ border-radius: 5px;
261
+ background: rgba(120, 120, 140, 0.12);
262
+ color: #6e6e76;
263
+ font: 500 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
264
+ white-space: nowrap;
265
+ user-select: none;
266
+ }
267
+ .field {
268
+ display: grid;
269
+ align-content: start;
270
+ gap: 4px;
271
+ min-width: 0;
272
+ }
273
+ /* text areas take a whole row */
274
+ .field.long {
275
+ grid-column: 1 / -1;
276
+ }
277
+ .line {
278
+ display: flex;
279
+ align-items: center;
280
+ gap: 8px;
281
+ min-width: 0;
282
+ }
283
+ .name {
284
+ font-size: 11.5px;
285
+ font-weight: 600;
286
+ color: #8e8e96;
287
+ white-space: nowrap;
288
+ user-select: none;
289
+ }
290
+ /* the view a per-view field edits (the host's current one); dashed while it only takes a wider view's value */
291
+ .view {
292
+ margin-left: 6px;
293
+ padding: 0 5px;
294
+ border: 1px dashed #c8c8d0;
295
+ border-radius: 4px;
296
+ color: #6e6e76;
297
+ font-weight: 500;
298
+ }
299
+ .view.own {
300
+ border-style: solid;
301
+ border-color: #1d1d1f;
302
+ color: #1d1d1f;
303
+ }
304
+ .slider {
305
+ display: block;
306
+ flex: 1;
307
+ min-width: 80px;
308
+ }
309
+ .value {
310
+ min-width: 4.5em;
311
+ color: #3a3a40;
312
+ font-variant-numeric: tabular-nums;
313
+ white-space: nowrap;
314
+ }
315
+ .nocolor {
316
+ display: flex;
317
+ align-items: center;
318
+ gap: 6px;
319
+ height: 30px;
320
+ padding: 0 10px 0 6px;
321
+ border: 1px dashed #c8c8d0;
322
+ border-radius: 7px;
323
+ background: #fff;
324
+ color: #8e8e96;
325
+ cursor: pointer;
326
+ }
327
+ .nocolor:hover {
328
+ border-color: var(--color-accent, #ea580c);
329
+ color: #3a3a40;
330
+ }
331
+ /* an empty swatch, struck through */
332
+ .swatch {
333
+ width: 16px;
334
+ height: 16px;
335
+ border: 1px solid #c8c8d0;
336
+ border-radius: 4px;
337
+ background: linear-gradient(to top right, transparent calc(50% - 1px), #e5484d calc(50% - 1px), #e5484d calc(50% + 1px), transparent calc(50% + 1px));
338
+ }
339
+ .clear {
340
+ width: 22px;
341
+ height: 22px;
342
+ padding: 0;
343
+ border: 0;
344
+ border-radius: 6px;
345
+ background: none;
346
+ color: #8e8e96;
347
+ font-size: 16px;
348
+ line-height: 1;
349
+ cursor: pointer;
350
+ }
351
+ .clear:hover {
352
+ background: rgba(120, 120, 140, 0.14);
353
+ color: #1d1d1f;
354
+ }
355
+ .chosen {
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 3px;
359
+ min-height: 30px;
360
+ padding: 2px;
361
+ border: 1px solid #dcdce2;
362
+ border-radius: 7px;
363
+ background: #fff;
364
+ cursor: pointer;
365
+ }
366
+ .chosen:hover {
367
+ border-color: var(--color-accent, #ea580c);
368
+ }
369
+ /* images after the sixth are a "+n" */
370
+ .chosen > img:nth-of-type(n + 7) {
371
+ display: none;
372
+ }
373
+ .chosen img,
374
+ .chosen .missing {
375
+ width: 40px;
376
+ height: 24px;
377
+ object-fit: cover;
378
+ border-radius: 4px;
379
+ }
380
+ .chosen .missing {
381
+ display: grid;
382
+ place-items: center;
383
+ background: #eee;
384
+ color: #8e8e96;
385
+ }
386
+ .chosen .more {
387
+ padding: 0 4px;
388
+ color: #6e6e76;
389
+ font-size: 11px;
390
+ }
391
+ .chosen .empty {
392
+ padding: 0 8px;
393
+ color: #8e8e96;
394
+ }
395
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { Translate } from './i18n';
2
+ import type { MediaItem } from './media';
3
+ import type { Block, PropField } from './types';
4
+ interface Props {
5
+ block: Block;
6
+ tag: string;
7
+ fields: PropField[];
8
+ media: MediaItem[];
9
+ tr: Translate;
10
+ /** Sets one property from its field's text and returns the text as it is now kept. */
11
+ setProp: (b: Block, f: PropField, raw: string) => string;
12
+ /** Sets one property to a value as it is ('' or undefined removes it). */
13
+ setValue: (b: Block, key: string, v: unknown) => void;
14
+ pickMedia: (b: Block, f: PropField, id: string) => void;
15
+ /** Start with the fields open (a block just made). */
16
+ open?: boolean;
17
+ }
18
+ declare const PropsBar: import("svelte").Component<Props, {}, "">;
19
+ type PropsBar = ReturnType<typeof PropsBar>;
20
+ export default PropsBar;
package/dist/dom.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /** Svelte action: sets innerHTML once and only re-syncs when the model differs from the DOM (keeps the caret). */
2
+ export declare function rich(node: HTMLElement, value: string): {
3
+ update(v: string): void;
4
+ };
5
+ export declare function serialize(frag: DocumentFragment): string;
6
+ /** Returns '' for html that has no visible text (e.g. `<b></b>`, `<br>`). */
7
+ export declare function clean(h: string): string;
8
+ export declare function textLen(h: string): number;
9
+ export declare function splitAtCaret(el: HTMLElement): [string, string] | null;
10
+ export declare function atStart(el: HTMLElement): boolean;
11
+ export declare function atEnd(el: HTMLElement): boolean;
12
+ export declare function caretOffset(el: HTMLElement): number;
13
+ export declare function setCaret(el: HTMLElement, where?: 'start' | 'end' | number): void;
14
+ /** Viewport rect of the caret, or null when the browser can't tell (empty block). */
15
+ export declare function caretRect(): DOMRect | null;
16
+ /** Places the caret in `el` at the viewport point (x, y). Keeps the horizontal position while moving between blocks. */
17
+ export declare function caretAt(el: HTMLElement, x: number, y: number): boolean;
18
+ /** Deletes `n` characters directly before the caret (inside the caret's text node). */
19
+ export declare function removeBeforeCaret(n: number): boolean;
20
+ /** Is the caret on the first (up) or last (down) visual line of the block? */
21
+ export declare function onEdgeLine(el: HTMLElement, up: boolean): boolean;
22
+ /**
23
+ * Plain text: a selection loses every mark and link; a caret steps out of the marks around it, so what is typed next
24
+ * is plain (a zero-width space holds it there; the markdown conversion drops it).
25
+ */
26
+ export declare function plainText(el: HTMLElement): void;
27
+ export declare function toggleCode(el: HTMLElement): void;
28
+ /** Is the selection's moving end (focus) at the very start (up) or end (down) of the block's text? */
29
+ export declare function focusAtEdge(el: HTMLElement, up: boolean): boolean;
package/dist/dom.js ADDED
@@ -0,0 +1,224 @@
1
+ /** Svelte action: sets innerHTML once and only re-syncs when the model differs from the DOM (keeps the caret). */
2
+ export function rich(node, value) {
3
+ node.innerHTML = value;
4
+ return {
5
+ update(v) {
6
+ if (node.innerHTML !== v)
7
+ node.innerHTML = v;
8
+ }
9
+ };
10
+ }
11
+ export function serialize(frag) {
12
+ const d = document.createElement('div');
13
+ d.append(frag);
14
+ return d.innerHTML;
15
+ }
16
+ /** Returns '' for html that has no visible text (e.g. `<b></b>`, `<br>`). */
17
+ export function clean(h) {
18
+ const d = document.createElement('div');
19
+ d.innerHTML = h;
20
+ return d.textContent ? h : '';
21
+ }
22
+ export function textLen(h) {
23
+ const d = document.createElement('div');
24
+ d.innerHTML = h;
25
+ return d.textContent?.length ?? 0;
26
+ }
27
+ function range() {
28
+ const sel = getSelection();
29
+ return sel && sel.rangeCount ? sel.getRangeAt(0) : null;
30
+ }
31
+ export function splitAtCaret(el) {
32
+ const r = range();
33
+ if (!r)
34
+ return null;
35
+ const pre = document.createRange();
36
+ pre.selectNodeContents(el);
37
+ pre.setEnd(r.startContainer, r.startOffset);
38
+ const post = document.createRange();
39
+ post.selectNodeContents(el);
40
+ post.setStart(r.endContainer, r.endOffset);
41
+ return [serialize(pre.cloneContents()), serialize(post.cloneContents())];
42
+ }
43
+ export function atStart(el) {
44
+ const r = range();
45
+ if (!r || !r.collapsed)
46
+ return false;
47
+ const pre = document.createRange();
48
+ pre.selectNodeContents(el);
49
+ pre.setEnd(r.startContainer, r.startOffset);
50
+ return pre.toString() === '';
51
+ }
52
+ export function atEnd(el) {
53
+ const r = range();
54
+ if (!r || !r.collapsed)
55
+ return false;
56
+ const post = document.createRange();
57
+ post.selectNodeContents(el);
58
+ post.setStart(r.endContainer, r.endOffset);
59
+ return post.toString() === '';
60
+ }
61
+ export function caretOffset(el) {
62
+ const r = range();
63
+ if (!r || !el.contains(r.startContainer))
64
+ return 0;
65
+ const pre = document.createRange();
66
+ pre.selectNodeContents(el);
67
+ pre.setEnd(r.startContainer, r.startOffset);
68
+ return pre.toString().length;
69
+ }
70
+ export function setCaret(el, where = 'end') {
71
+ el.focus({ preventScroll: true });
72
+ const sel = getSelection();
73
+ if (!sel)
74
+ return;
75
+ const r = document.createRange();
76
+ r.selectNodeContents(el);
77
+ if (where === 'start')
78
+ r.collapse(true);
79
+ else if (where === 'end')
80
+ r.collapse(false);
81
+ else {
82
+ r.collapse(false);
83
+ const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
84
+ let left = where;
85
+ let n;
86
+ while ((n = w.nextNode())) {
87
+ const len = n.textContent?.length ?? 0;
88
+ if (left <= len) {
89
+ r.setStart(n, left);
90
+ r.collapse(true);
91
+ break;
92
+ }
93
+ left -= len;
94
+ }
95
+ }
96
+ sel.removeAllRanges();
97
+ sel.addRange(r);
98
+ el.scrollIntoView({ block: 'nearest' });
99
+ }
100
+ /** Viewport rect of the caret, or null when the browser can't tell (empty block). */
101
+ export function caretRect() {
102
+ const r = range();
103
+ if (!r)
104
+ return null;
105
+ const c = r.cloneRange();
106
+ c.collapse(false);
107
+ const rects = c.getClientRects();
108
+ const rect = rects.length ? rects[rects.length - 1] : c.getBoundingClientRect();
109
+ return rect && (rect.width || rect.height) ? rect : null;
110
+ }
111
+ /** Places the caret in `el` at the viewport point (x, y). Keeps the horizontal position while moving between blocks. */
112
+ export function caretAt(el, x, y) {
113
+ const d = document;
114
+ let r = null;
115
+ if (d.caretPositionFromPoint) {
116
+ const p = d.caretPositionFromPoint(x, y);
117
+ if (p) {
118
+ r = document.createRange();
119
+ r.setStart(p.offsetNode, p.offset);
120
+ }
121
+ }
122
+ else if (d.caretRangeFromPoint) {
123
+ r = d.caretRangeFromPoint(x, y);
124
+ }
125
+ if (!r || !el.contains(r.startContainer))
126
+ return false;
127
+ r.collapse(true);
128
+ el.focus({ preventScroll: true });
129
+ const sel = getSelection();
130
+ sel.removeAllRanges();
131
+ sel.addRange(r);
132
+ el.scrollIntoView({ block: 'nearest' });
133
+ return true;
134
+ }
135
+ /** Deletes `n` characters directly before the caret (inside the caret's text node). */
136
+ export function removeBeforeCaret(n) {
137
+ const r = range();
138
+ if (!r || r.startContainer.nodeType !== Node.TEXT_NODE || r.startOffset < n)
139
+ return false;
140
+ const t = r.startContainer;
141
+ const off = r.startOffset;
142
+ t.deleteData(off - n, n);
143
+ const nr = document.createRange();
144
+ nr.setStart(t, off - n);
145
+ nr.collapse(true);
146
+ const sel = getSelection();
147
+ sel.removeAllRanges();
148
+ sel.addRange(nr);
149
+ return true;
150
+ }
151
+ /** Is the caret on the first (up) or last (down) visual line of the block? */
152
+ export function onEdgeLine(el, up) {
153
+ const sel = getSelection();
154
+ if (!sel || !sel.isCollapsed)
155
+ return false;
156
+ const cr = caretRect();
157
+ if (!cr)
158
+ return true;
159
+ const er = el.getBoundingClientRect();
160
+ const lh = parseFloat(getComputedStyle(el).lineHeight) || 24;
161
+ return up ? cr.top - er.top < lh * 0.5 : er.bottom - cr.bottom < lh * 0.5;
162
+ }
163
+ const MARKS = new Set(['B', 'STRONG', 'I', 'EM', 'U', 'S', 'STRIKE', 'DEL', 'SUB', 'SUP', 'CODE', 'A']);
164
+ /**
165
+ * Plain text: a selection loses every mark and link; a caret steps out of the marks around it, so what is typed next
166
+ * is plain (a zero-width space holds it there; the markdown conversion drops it).
167
+ */
168
+ export function plainText(el) {
169
+ const sel = getSelection();
170
+ const r = range();
171
+ if (!sel || !r || !el.contains(r.commonAncestorContainer))
172
+ return;
173
+ if (!r.collapsed) {
174
+ document.execCommand('removeFormat');
175
+ document.execCommand('unlink');
176
+ return;
177
+ }
178
+ let outer = null;
179
+ for (let n = r.startContainer; n && n !== el; n = n.parentNode)
180
+ if (MARKS.has(n.nodeName))
181
+ outer = n;
182
+ if (!outer)
183
+ return;
184
+ const t = document.createTextNode('\u200B');
185
+ outer.after(t);
186
+ const nr = document.createRange();
187
+ nr.setStart(t, 1);
188
+ nr.collapse(true);
189
+ sel.removeAllRanges();
190
+ sel.addRange(nr);
191
+ }
192
+ export function toggleCode(el) {
193
+ const sel = getSelection();
194
+ const r = range();
195
+ if (!sel || !r || r.collapsed)
196
+ return;
197
+ const anc = r.commonAncestorContainer;
198
+ const host = (anc instanceof Element ? anc : anc.parentElement)?.closest('code');
199
+ if (host && el.contains(host)) {
200
+ const nodes = [...host.childNodes];
201
+ host.replaceWith(...nodes);
202
+ return;
203
+ }
204
+ const c = document.createElement('code');
205
+ c.append(r.extractContents());
206
+ r.insertNode(c);
207
+ const nr = document.createRange();
208
+ nr.selectNodeContents(c);
209
+ sel.removeAllRanges();
210
+ sel.addRange(nr);
211
+ }
212
+ /** Is the selection's moving end (focus) at the very start (up) or end (down) of the block's text? */
213
+ export function focusAtEdge(el, up) {
214
+ const sel = getSelection();
215
+ if (!sel || !sel.focusNode || !el.contains(sel.focusNode))
216
+ return false;
217
+ const r = document.createRange();
218
+ r.selectNodeContents(el);
219
+ if (up)
220
+ r.setEnd(sel.focusNode, sel.focusOffset);
221
+ else
222
+ r.setStart(sel.focusNode, sel.focusOffset);
223
+ return r.toString() === '';
224
+ }
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type Messages = Record<string, string>;
2
+ export type Translate = (text: string, vars?: Record<string, string | number>) => string;
3
+ export declare const translator: (messages?: Messages) => Translate;
4
+ /** Call in a component's init. Without its own messages, a nested editor or preview keeps its parent's. */
5
+ export declare function provideText(own: () => Messages | undefined): void;
6
+ /** Call in a component's init; the returned function translates with the current messages. */
7
+ export declare function useText(): Translate;
package/dist/i18n.js ADDED
@@ -0,0 +1,18 @@
1
+ // UI text. The editor and the plugins are written in English; a host passes `messages` (English text -> its own) to
2
+ // the top editor or preview, and nested editors and plugin views inherit them. `{name}` placeholders are filled in.
3
+ import { getContext, setContext } from 'svelte';
4
+ const KEY = 'block-editor:text';
5
+ export const translator = (messages) => (text, vars) => {
6
+ const out = messages?.[text] ?? text;
7
+ return vars ? out.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m)) : out;
8
+ };
9
+ /** Call in a component's init. Without its own messages, a nested editor or preview keeps its parent's. */
10
+ export function provideText(own) {
11
+ const parent = getContext(KEY);
12
+ setContext(KEY, () => own() ?? parent?.());
13
+ }
14
+ /** Call in a component's init; the returned function translates with the current messages. */
15
+ export function useText() {
16
+ const get = getContext(KEY);
17
+ return (text, vars) => translator(get?.())(text, vars);
18
+ }