@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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README-AI.md +283 -0
- package/README.md +41 -2
- package/dist/Editor.svelte +1968 -0
- package/dist/Editor.svelte.d.ts +22 -0
- package/dist/ImagePicker.svelte +131 -0
- package/dist/ImagePicker.svelte.d.ts +13 -0
- package/dist/LinkPrompt.svelte +42 -0
- package/dist/LinkPrompt.svelte.d.ts +8 -0
- package/dist/PluginHost.svelte +41 -0
- package/dist/PluginHost.svelte.d.ts +13 -0
- package/dist/Preview.svelte +186 -0
- package/dist/Preview.svelte.d.ts +35 -0
- package/dist/PropsBar.svelte +395 -0
- package/dist/PropsBar.svelte.d.ts +20 -0
- package/dist/dom.d.ts +29 -0
- package/dist/dom.js +224 -0
- package/dist/i18n.d.ts +7 -0
- package/dist/i18n.js +18 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +16 -0
- package/dist/kit/Empty.svelte +24 -0
- package/dist/kit/Empty.svelte.d.ts +8 -0
- package/dist/kit/Frame.svelte +76 -0
- package/dist/kit/Frame.svelte.d.ts +15 -0
- package/dist/kit/util.d.ts +13 -0
- package/dist/kit/util.js +28 -0
- package/dist/locales/hu.d.ts +2 -0
- package/dist/locales/hu.js +147 -0
- package/dist/markdown.d.ts +25 -0
- package/dist/markdown.js +219 -0
- package/dist/media.d.ts +17 -0
- package/dist/media.js +12 -0
- package/dist/model.d.ts +21 -0
- package/dist/model.js +45 -0
- package/dist/palette.d.ts +19 -0
- package/dist/palette.js +23 -0
- package/dist/plugins/Gap.svelte +28 -0
- package/dist/plugins/Gap.svelte.d.ts +15 -0
- package/dist/plugins/GapPreview.svelte +17 -0
- package/dist/plugins/GapPreview.svelte.d.ts +5 -0
- package/dist/plugins/Map.svelte +47 -0
- package/dist/plugins/Map.svelte.d.ts +12 -0
- package/dist/plugins/MapPreview.svelte +26 -0
- package/dist/plugins/MapPreview.svelte.d.ts +5 -0
- package/dist/plugins/Table.svelte +563 -0
- package/dist/plugins/Table.svelte.d.ts +28 -0
- package/dist/plugins/TablePreview.svelte +62 -0
- package/dist/plugins/TablePreview.svelte.d.ts +5 -0
- package/dist/plugins/YouTube.svelte +52 -0
- package/dist/plugins/YouTube.svelte.d.ts +8 -0
- package/dist/plugins/YouTubePreview.svelte +76 -0
- package/dist/plugins/YouTubePreview.svelte.d.ts +5 -0
- package/dist/plugins/index.d.ts +20 -0
- package/dist/plugins/index.js +94 -0
- package/dist/plugins/util.d.ts +3 -0
- package/dist/plugins/util.js +8 -0
- package/dist/schema.d.ts +11 -0
- package/dist/schema.js +25 -0
- package/dist/scroll.d.ts +18 -0
- package/dist/scroll.js +57 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.js +43 -0
- package/dist/view.d.ts +6 -0
- package/dist/view.js +11 -0
- package/docs/en/bundled-plugins.md +99 -0
- package/docs/en/documents-and-markdown.md +115 -0
- package/docs/en/editor-and-preview.md +141 -0
- package/docs/en/getting-started.md +81 -0
- package/docs/en/host-contexts.md +275 -0
- package/docs/en/index.md +38 -0
- package/docs/en/recipes-and-pitfalls.md +122 -0
- package/docs/en/server-side.md +95 -0
- package/docs/en/upgrading.md +55 -0
- package/docs/en/writing-plugins.md +305 -0
- package/docs/hu/bundled-plugins.md +99 -0
- package/docs/hu/documents-and-markdown.md +115 -0
- package/docs/hu/editor-and-preview.md +141 -0
- package/docs/hu/getting-started.md +81 -0
- package/docs/hu/host-contexts.md +275 -0
- package/docs/hu/index.md +38 -0
- package/docs/hu/recipes-and-pitfalls.md +122 -0
- package/docs/hu/server-side.md +95 -0
- package/docs/hu/upgrading.md +55 -0
- package/docs/hu/writing-plugins.md +305 -0
- package/package.json +110 -5
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface YouTubeProps extends Record<string, unknown> {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
}
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { Play, SquarePlay } from '@lucide/svelte';
|
|
10
|
+
import type { PluginViewProps } from '../types';
|
|
11
|
+
import Empty from '../kit/Empty.svelte';
|
|
12
|
+
|
|
13
|
+
let { props = $bindable() }: PluginViewProps<YouTubeProps> = $props();
|
|
14
|
+
const id = $derived(/^[\w-]{11}$/.test(props.id ?? '') ? props.id : '');
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
{#if id}
|
|
18
|
+
<div class="yt">
|
|
19
|
+
<img src="https://i.ytimg.com/vi/{id}/hqdefault.jpg" alt={props.title} draggable="false" />
|
|
20
|
+
<span class="play"><Play size={22} /></span>
|
|
21
|
+
</div>
|
|
22
|
+
{:else}
|
|
23
|
+
<Empty icon={SquarePlay} text="Paste a YouTube link above" />
|
|
24
|
+
{/if}
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.yt {
|
|
28
|
+
position: relative;
|
|
29
|
+
border-radius: 10px;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
background: #000;
|
|
32
|
+
}
|
|
33
|
+
img {
|
|
34
|
+
display: block;
|
|
35
|
+
width: 100%;
|
|
36
|
+
aspect-ratio: 16 / 9;
|
|
37
|
+
object-fit: cover;
|
|
38
|
+
opacity: 0.85;
|
|
39
|
+
}
|
|
40
|
+
.play {
|
|
41
|
+
position: absolute;
|
|
42
|
+
inset: 0;
|
|
43
|
+
margin: auto;
|
|
44
|
+
width: 56px;
|
|
45
|
+
height: 40px;
|
|
46
|
+
display: grid;
|
|
47
|
+
place-items: center;
|
|
48
|
+
border-radius: 10px;
|
|
49
|
+
background: #dc2626;
|
|
50
|
+
color: #fff;
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface YouTubeProps extends Record<string, unknown> {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
}
|
|
5
|
+
import type { PluginViewProps } from '../types';
|
|
6
|
+
declare const YouTube: import("svelte").Component<PluginViewProps<YouTubeProps>, {}, "props">;
|
|
7
|
+
type YouTube = ReturnType<typeof YouTube>;
|
|
8
|
+
export default YouTube;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<svelte:options css="injected" />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { useText } from '../i18n';
|
|
5
|
+
import type { PluginPreviewProps } from '../types';
|
|
6
|
+
import type { YouTubeProps } from './YouTube.svelte';
|
|
7
|
+
|
|
8
|
+
let { props }: PluginPreviewProps<YouTubeProps> = $props();
|
|
9
|
+
const tr = useText();
|
|
10
|
+
const id = $derived(/^[\w-]{11}$/.test(props.id ?? '') ? props.id : '');
|
|
11
|
+
// The player loads only on click (no YouTube request before that but the thumbnail).
|
|
12
|
+
let playing = $state(false);
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#if id}
|
|
16
|
+
<figure class="yt">
|
|
17
|
+
{#if playing}
|
|
18
|
+
<iframe
|
|
19
|
+
src="https://www.youtube-nocookie.com/embed/{id}?autoplay=1"
|
|
20
|
+
title={props.title || tr('YouTube video')}
|
|
21
|
+
allow="autoplay; encrypted-media; picture-in-picture"
|
|
22
|
+
allowfullscreen
|
|
23
|
+
></iframe>
|
|
24
|
+
{:else}
|
|
25
|
+
<button type="button" onclick={() => (playing = true)} aria-label={tr('Play {title}', { title: props.title || tr('video') })}>
|
|
26
|
+
<img src="https://i.ytimg.com/vi/{id}/hqdefault.jpg" alt="" />
|
|
27
|
+
<span class="play" aria-hidden="true">▶</span>
|
|
28
|
+
</button>
|
|
29
|
+
{/if}
|
|
30
|
+
{#if props.title}<figcaption>{props.title}</figcaption>{/if}
|
|
31
|
+
</figure>
|
|
32
|
+
{/if}
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
.yt {
|
|
36
|
+
margin: 0 0 1em;
|
|
37
|
+
}
|
|
38
|
+
iframe,
|
|
39
|
+
button {
|
|
40
|
+
display: block;
|
|
41
|
+
width: 100%;
|
|
42
|
+
aspect-ratio: 16 / 9;
|
|
43
|
+
border: 0;
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
}
|
|
46
|
+
button {
|
|
47
|
+
position: relative;
|
|
48
|
+
padding: 0;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
background: #000;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
}
|
|
53
|
+
img {
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 100%;
|
|
56
|
+
object-fit: cover;
|
|
57
|
+
}
|
|
58
|
+
.play {
|
|
59
|
+
position: absolute;
|
|
60
|
+
inset: 0;
|
|
61
|
+
margin: auto;
|
|
62
|
+
width: 64px;
|
|
63
|
+
height: 44px;
|
|
64
|
+
display: grid;
|
|
65
|
+
place-items: center;
|
|
66
|
+
border-radius: 12px;
|
|
67
|
+
background: #dc2626;
|
|
68
|
+
color: #fff;
|
|
69
|
+
font-size: 20px;
|
|
70
|
+
}
|
|
71
|
+
figcaption {
|
|
72
|
+
margin-top: 0.4em;
|
|
73
|
+
color: #6b6b76;
|
|
74
|
+
font: 13px/1.4 system-ui, sans-serif;
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PluginPreviewProps } from '../types';
|
|
2
|
+
import type { YouTubeProps } from './YouTube.svelte';
|
|
3
|
+
declare const YouTubePreview: import("svelte").Component<PluginPreviewProps<YouTubeProps>, {}, "">;
|
|
4
|
+
type YouTubePreview = ReturnType<typeof YouTubePreview>;
|
|
5
|
+
export default YouTubePreview;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BlockPlugin } from '../types';
|
|
2
|
+
import { type GapProps } from './Gap.svelte';
|
|
3
|
+
import { type MapProps } from './Map.svelte';
|
|
4
|
+
import { type TableProps } from './Table.svelte';
|
|
5
|
+
import { type YouTubeProps } from './YouTube.svelte';
|
|
6
|
+
/** A YouTube video, loaded on click. */
|
|
7
|
+
export declare const youtube: BlockPlugin<YouTubeProps>;
|
|
8
|
+
/** Rows and columns of inline-formatted cells (price lists, fines…); edited in place, rows and columns dragged by their
|
|
9
|
+
* grips. The first row can be a header, the last a footer (sums). */
|
|
10
|
+
export declare const table: BlockPlugin<TableProps>;
|
|
11
|
+
/** A Google Maps embed of a place or an address. */
|
|
12
|
+
export declare const map: BlockPlugin<MapProps>;
|
|
13
|
+
/** Empty space between blocks, some lines of text high; only its height can be set. */
|
|
14
|
+
export declare const gap: BlockPlugin<GapProps>;
|
|
15
|
+
export declare const PLUGINS: BlockPlugin[];
|
|
16
|
+
export { youtubeId } from './util';
|
|
17
|
+
export type { GapProps, MapProps, TableProps, YouTubeProps };
|
|
18
|
+
export { square, tableHeads, rowHead, HEADS, type Head } from './Table.svelte';
|
|
19
|
+
export { GAP_LINES, GAP_MAX, gapLines } from './Gap.svelte';
|
|
20
|
+
export { mapSrc } from './Map.svelte';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The bundled plugins: plugins like any other (written with the kit), an app picks the ones it wants. Only general
|
|
2
|
+
// blocks live here (video, map, table, gap); anything an app gives its own meaning to belongs in the app.
|
|
3
|
+
import { MapPin, SquarePlay, Table, UnfoldVertical } from '@lucide/svelte';
|
|
4
|
+
import { mdText } from '../kit/util';
|
|
5
|
+
import { youtubeId } from './util';
|
|
6
|
+
import Gap, { GAP_LINES } from './Gap.svelte';
|
|
7
|
+
import GapPreview from './GapPreview.svelte';
|
|
8
|
+
import MapView, {} from './Map.svelte';
|
|
9
|
+
import MapPreview from './MapPreview.svelte';
|
|
10
|
+
import TableView, { tableHeads } from './Table.svelte';
|
|
11
|
+
import TablePreview from './TablePreview.svelte';
|
|
12
|
+
import YouTube, {} from './YouTube.svelte';
|
|
13
|
+
import YouTubePreview from './YouTubePreview.svelte';
|
|
14
|
+
// Markdown forms (BlockPlugin.markdown).
|
|
15
|
+
const mdLink = (text, url) => `[${mdText(text) || url}](${url})`;
|
|
16
|
+
/** One table row in GFM; the cells are inline markdown already, only the pipes are escaped. */
|
|
17
|
+
const mdRow = (cells) => `| ${cells.map((c) => (c ?? '').replace(/\|/g, '\\|').replace(/\n/g, '<br>')).join(' | ')} |`;
|
|
18
|
+
/** A YouTube video, loaded on click. */
|
|
19
|
+
export const youtube = {
|
|
20
|
+
type: 'youtube',
|
|
21
|
+
label: 'YouTube',
|
|
22
|
+
hint: 'Video from a link',
|
|
23
|
+
keys: 'youtube video embed',
|
|
24
|
+
icon: SquarePlay,
|
|
25
|
+
props: () => ({ id: '', title: '' }),
|
|
26
|
+
fields: [
|
|
27
|
+
{ key: 'id', label: 'Video link or id', kind: 'text', placeholder: 'https://youtu.be/…', parse: youtubeId },
|
|
28
|
+
{ key: 'title', label: 'Title', kind: 'text' }
|
|
29
|
+
],
|
|
30
|
+
tag: () => 'youtube',
|
|
31
|
+
content: 'none',
|
|
32
|
+
markdown: (p) => (p.id ? mdLink(p.title, `https://www.youtube.com/watch?v=${p.id}`) : ''),
|
|
33
|
+
view: YouTube,
|
|
34
|
+
preview: YouTubePreview
|
|
35
|
+
};
|
|
36
|
+
/** Rows and columns of inline-formatted cells (price lists, fines…); edited in place, rows and columns dragged by their
|
|
37
|
+
* grips. The first row can be a header, the last a footer (sums). */
|
|
38
|
+
export const table = {
|
|
39
|
+
type: 'table',
|
|
40
|
+
label: 'Table',
|
|
41
|
+
hint: 'Rows and columns',
|
|
42
|
+
keys: 'table grid rows columns price list',
|
|
43
|
+
icon: Table,
|
|
44
|
+
props: () => ({ rows: [['', ''], ['', '']], heads: ['top'] }),
|
|
45
|
+
tag: (p) => `table · ${p.rows?.length ?? 0}×${p.rows?.[0]?.length ?? 0}`,
|
|
46
|
+
content: 'none',
|
|
47
|
+
markdown: (p) => {
|
|
48
|
+
const rows = p.rows ?? [];
|
|
49
|
+
if (!rows.length)
|
|
50
|
+
return '';
|
|
51
|
+
const body = tableHeads(p).has('top') ? rows.slice(1) : rows;
|
|
52
|
+
const head = tableHeads(p).has('top') ? rows[0] : rows[0].map(() => '');
|
|
53
|
+
return [mdRow(head), mdRow(head.map(() => '---')), ...body.map(mdRow)].join('\n');
|
|
54
|
+
},
|
|
55
|
+
view: TableView,
|
|
56
|
+
preview: TablePreview
|
|
57
|
+
};
|
|
58
|
+
/** A Google Maps embed of a place or an address. */
|
|
59
|
+
export const map = {
|
|
60
|
+
type: 'map',
|
|
61
|
+
label: 'Map',
|
|
62
|
+
hint: 'Google Maps for a place',
|
|
63
|
+
keys: 'map google location address place',
|
|
64
|
+
icon: MapPin,
|
|
65
|
+
props: () => ({ q: '', zoom: '15' }),
|
|
66
|
+
fields: [
|
|
67
|
+
{ key: 'q', label: 'Place or address', kind: 'text', placeholder: 'e.g. Pécs, Széchenyi tér' },
|
|
68
|
+
{ key: 'zoom', label: 'Zoom', kind: 'slider', options: Array.from({ length: 20 }, (_, i) => ({ value: String(i + 1), label: String(i + 1) })) }
|
|
69
|
+
],
|
|
70
|
+
tag: (p) => (p.q ? `map · ${p.q}` : 'map'),
|
|
71
|
+
content: 'none',
|
|
72
|
+
markdown: (p) => (p.q ? mdLink(p.q, `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(String(p.q))}`) : ''),
|
|
73
|
+
view: MapView,
|
|
74
|
+
preview: MapPreview
|
|
75
|
+
};
|
|
76
|
+
/** Empty space between blocks, some lines of text high; only its height can be set. */
|
|
77
|
+
export const gap = {
|
|
78
|
+
type: 'gap',
|
|
79
|
+
label: 'Gap',
|
|
80
|
+
hint: 'Empty space',
|
|
81
|
+
keys: 'gap space spacer whitespace',
|
|
82
|
+
icon: UnfoldVertical,
|
|
83
|
+
props: () => ({ lines: '1' }),
|
|
84
|
+
fields: [{ key: 'lines', label: 'Height', kind: 'slider', options: GAP_LINES }],
|
|
85
|
+
tag: (p) => `gap · ${p.lines ?? '1'}`,
|
|
86
|
+
content: 'none',
|
|
87
|
+
view: Gap,
|
|
88
|
+
preview: GapPreview
|
|
89
|
+
};
|
|
90
|
+
export const PLUGINS = [table, youtube, map, gap];
|
|
91
|
+
export { youtubeId } from './util';
|
|
92
|
+
export { square, tableHeads, rowHead, HEADS } from './Table.svelte';
|
|
93
|
+
export { GAP_LINES, GAP_MAX, gapLines } from './Gap.svelte';
|
|
94
|
+
export { mapSrc } from './Map.svelte';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Helpers of the bundled plugins (the general ones are in kit/util.ts).
|
|
2
|
+
export { lines, safeColor } from '../kit/util';
|
|
3
|
+
/** A YouTube video id from an id or any YouTube link. */
|
|
4
|
+
export function youtubeId(s) {
|
|
5
|
+
const t = s.trim();
|
|
6
|
+
const m = /(?:v=|youtu\.be\/|embed\/|shorts\/)([\w-]{11})/.exec(t);
|
|
7
|
+
return m ? m[1] : t.replace(/[^\w-]/g, '').slice(0, 11);
|
|
8
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type StoredBlock } from './model';
|
|
3
|
+
export declare const blocksSchema: z.ZodArray<z.ZodType<StoredBlock, unknown, z.core.$ZodTypeInternals<StoredBlock, unknown>>>;
|
|
4
|
+
/**
|
|
5
|
+
* The stored blocks, also checked against the containers' `accepts` (pass the editor's plugins, or just their
|
|
6
|
+
* `type` and `accepts`): a container may hold only the blocks it takes.
|
|
7
|
+
*/
|
|
8
|
+
export declare const blocksSchemaFor: (plugins: readonly {
|
|
9
|
+
type: string;
|
|
10
|
+
accepts?: readonly string[];
|
|
11
|
+
}[]) => z.ZodArray<z.ZodType<StoredBlock, unknown, z.core.$ZodTypeInternals<StoredBlock, unknown>>>;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Server-side validation of a stored document (zod). Only the shape is checked: the renderers escape every text,
|
|
2
|
+
// keep links to http(s)/mailto/tel/site paths, take colours only as #hex and images only from the host's media.
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { acceptsId, MAX_DEPTH, MAX_INDENT } from './model';
|
|
5
|
+
const block = z.lazy(() => z.strictObject({
|
|
6
|
+
type: z.string().regex(/^[a-z][a-z0-9:-]{0,39}$/),
|
|
7
|
+
indent: z.number().int().min(0).max(MAX_INDENT),
|
|
8
|
+
text: z.string().max(100_000).optional(),
|
|
9
|
+
props: z.record(z.string().max(60), z.json()).optional(),
|
|
10
|
+
blocks: z.array(block).max(500).optional()
|
|
11
|
+
}));
|
|
12
|
+
const depth = (blocks) => Math.max(0, ...blocks.map((b) => (b.blocks ? 1 + depth(b.blocks) : 0)));
|
|
13
|
+
export const blocksSchema = z
|
|
14
|
+
.array(block)
|
|
15
|
+
.max(2000)
|
|
16
|
+
.refine((blocks) => depth(blocks) <= MAX_DEPTH - 1, 'Blocks may hold blocks one level deep only.')
|
|
17
|
+
.refine((blocks) => JSON.stringify(blocks).length <= 1_000_000, 'The content is too large.');
|
|
18
|
+
/**
|
|
19
|
+
* The stored blocks, also checked against the containers' `accepts` (pass the editor's plugins, or just their
|
|
20
|
+
* `type` and `accepts`): a container may hold only the blocks it takes.
|
|
21
|
+
*/
|
|
22
|
+
export const blocksSchemaFor = (plugins) => {
|
|
23
|
+
const accepts = new Map(plugins.map((p) => [p.type, p.accepts]));
|
|
24
|
+
return blocksSchema.refine((blocks) => blocks.every((b) => !b.blocks || b.blocks.every((inner) => acceptsId(accepts.get(b.type), inner.type))), 'A block holds a block it does not take.');
|
|
25
|
+
};
|
package/dist/scroll.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Action } from 'svelte/action';
|
|
2
|
+
/** How far a box is scrolled, 0…1 (0 when it does not scroll). */
|
|
3
|
+
export declare const scrollFraction: (box: HTMLElement) => number;
|
|
4
|
+
/** Scrolls a box to a fraction (0…1) of its scroll range. */
|
|
5
|
+
export declare const setScrollFraction: (box: HTMLElement, f: number) => void;
|
|
6
|
+
export interface ScrollSync {
|
|
7
|
+
/** The box the reader scrolls (e.g. the editor's). */
|
|
8
|
+
source: Action<HTMLElement>;
|
|
9
|
+
/**
|
|
10
|
+
* The box that follows it (e.g. the preview's); its content changing size keeps it in step too. Its parameter turns
|
|
11
|
+
* it on and off (default on), for a host that switches between two boxes.
|
|
12
|
+
*/
|
|
13
|
+
target: Action<HTMLElement, boolean | undefined>;
|
|
14
|
+
/** Puts the target in step now (e.g. after its width changed). */
|
|
15
|
+
align(): void;
|
|
16
|
+
}
|
|
17
|
+
/** Keeps a target box scrolled to the same fraction as the source box. One way: scrolling the target moves nothing else. */
|
|
18
|
+
export declare function scrollSync(): ScrollSync;
|
package/dist/scroll.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** How far a box is scrolled, 0…1 (0 when it does not scroll). */
|
|
2
|
+
export const scrollFraction = (box) => {
|
|
3
|
+
const max = box.scrollHeight - box.clientHeight;
|
|
4
|
+
return max > 0 ? box.scrollTop / max : 0;
|
|
5
|
+
};
|
|
6
|
+
/** Scrolls a box to a fraction (0…1) of its scroll range. */
|
|
7
|
+
export const setScrollFraction = (box, f) => {
|
|
8
|
+
box.scrollTop = Math.min(1, Math.max(0, f)) * (box.scrollHeight - box.clientHeight);
|
|
9
|
+
};
|
|
10
|
+
/** Keeps a target box scrolled to the same fraction as the source box. One way: scrolling the target moves nothing else. */
|
|
11
|
+
export function scrollSync() {
|
|
12
|
+
let source = null;
|
|
13
|
+
let target = null;
|
|
14
|
+
let frame = 0;
|
|
15
|
+
const align = () => {
|
|
16
|
+
frame = 0;
|
|
17
|
+
if (source && target)
|
|
18
|
+
setScrollFraction(target, scrollFraction(source));
|
|
19
|
+
};
|
|
20
|
+
const later = () => (frame ||= requestAnimationFrame(align));
|
|
21
|
+
return {
|
|
22
|
+
source(node) {
|
|
23
|
+
source = node;
|
|
24
|
+
node.addEventListener('scroll', later, { passive: true });
|
|
25
|
+
return {
|
|
26
|
+
destroy() {
|
|
27
|
+
node.removeEventListener('scroll', later);
|
|
28
|
+
if (source === node)
|
|
29
|
+
source = null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
target(node, on = true) {
|
|
34
|
+
const watch = new ResizeObserver(later);
|
|
35
|
+
const use = (yes = true) => {
|
|
36
|
+
watch.disconnect();
|
|
37
|
+
if (!yes)
|
|
38
|
+
return void (target === node && (target = null));
|
|
39
|
+
target = node;
|
|
40
|
+
for (const child of node.children)
|
|
41
|
+
watch.observe(child);
|
|
42
|
+
watch.observe(node);
|
|
43
|
+
later();
|
|
44
|
+
};
|
|
45
|
+
use(on);
|
|
46
|
+
return {
|
|
47
|
+
update: use,
|
|
48
|
+
destroy() {
|
|
49
|
+
watch.disconnect();
|
|
50
|
+
if (target === node)
|
|
51
|
+
target = null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
align
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { MediaItem } from './media';
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
import type { StoredBlock } from './model';
|
|
4
|
+
import type { Icon } from './palette';
|
|
5
|
+
export type BlockType = 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'ul' | 'ol' | 'todo' | 'quote' | 'code' | 'hr' | 'plugin';
|
|
6
|
+
export type Props = Record<string, unknown>;
|
|
7
|
+
export interface Block {
|
|
8
|
+
/** Editor-only identity (rendering, focus, selection, drag); never stored. */
|
|
9
|
+
id: string;
|
|
10
|
+
type: BlockType;
|
|
11
|
+
/** Inline HTML (b, i, u, strike, code, a, br). Always sanitized by construction, never raw paste. */
|
|
12
|
+
html: string;
|
|
13
|
+
indent: number;
|
|
14
|
+
checked?: boolean;
|
|
15
|
+
/** Properties edited in the block's fields: a code block's `lang`, or a plugin's own. */
|
|
16
|
+
props?: Props;
|
|
17
|
+
/** Plugin blocks only: the plugin's stored type and its nested document. */
|
|
18
|
+
plugin?: string;
|
|
19
|
+
blocks?: StoredBlock[];
|
|
20
|
+
}
|
|
21
|
+
export { isMd, MD, type StoredBlock } from './model';
|
|
22
|
+
/**
|
|
23
|
+
* What a block holds: nothing (divider), inline-formatted text (paragraph, heading, list item…), plain text
|
|
24
|
+
* without formatting (code), or a nested list of blocks (box).
|
|
25
|
+
*/
|
|
26
|
+
export type Content = 'none' | 'text' | 'plain' | 'blocks';
|
|
27
|
+
/** Content of the built-in block types. */
|
|
28
|
+
export declare const BUILT_IN_CONTENT: Record<Exclude<BlockType, 'plugin'>, Content>;
|
|
29
|
+
/** One editable property, a field on the top of the block. */
|
|
30
|
+
export interface PropField {
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
/**
|
|
34
|
+
* 'image' picks one of the document's images (stores its id), 'images' several (an array of ids, in picking order).
|
|
35
|
+
* 'slider' steps through `options` in their order (e.g. heights), or, with `min` and `max`, sets a number freely.
|
|
36
|
+
*/
|
|
37
|
+
kind: 'text' | 'textarea' | 'color' | 'select' | 'slider' | 'image' | 'images';
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
/** A number slider: its range, step (default 1) and unit shown after the value. */
|
|
40
|
+
min?: number;
|
|
41
|
+
max?: number;
|
|
42
|
+
step?: number;
|
|
43
|
+
unit?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The label of the unset state, where the key is removed: a number slider's position left of `min` (e.g. 'Original'),
|
|
46
|
+
* or an optional colour ('None'; `placeholder` is the colour a click on it starts from).
|
|
47
|
+
*/
|
|
48
|
+
none?: string;
|
|
49
|
+
/**
|
|
50
|
+
* A slider with a value per view (e.g. the page's widths); the host sets the current view with `provideView`, a badge
|
|
51
|
+
* shows which one is edited. The first view is
|
|
52
|
+
* the base; each other view, while it has no value of its own, takes the one before it. Stored as the base value
|
|
53
|
+
* alone, or, with own values for other views, as an object by view key.
|
|
54
|
+
*/
|
|
55
|
+
views?: {
|
|
56
|
+
key: string;
|
|
57
|
+
label: string;
|
|
58
|
+
}[];
|
|
59
|
+
/** For 'select' and 'slider'. */
|
|
60
|
+
options?: {
|
|
61
|
+
value: string;
|
|
62
|
+
label: string;
|
|
63
|
+
}[];
|
|
64
|
+
/** The field's text from the stored value; default: the value as a string. */
|
|
65
|
+
format?: (value: unknown) => string;
|
|
66
|
+
/** The stored value from the field's text (e.g. a list from lines, an id from a URL); default: the text. '' removes the key. */
|
|
67
|
+
parse?: (text: string) => unknown;
|
|
68
|
+
}
|
|
69
|
+
/** What a plugin component receives. `props` and `blocks` are bindable. */
|
|
70
|
+
export interface PluginViewProps<P extends Props = Props> {
|
|
71
|
+
props: P;
|
|
72
|
+
blocks: StoredBlock[];
|
|
73
|
+
readonly: boolean;
|
|
74
|
+
/** The host editor's palette and plugins, for a nested editor. */
|
|
75
|
+
palette: string[];
|
|
76
|
+
plugins: BlockPlugin[];
|
|
77
|
+
}
|
|
78
|
+
/** What a plugin's preview receives: the block as stored, and the plugins to render its nested blocks with. */
|
|
79
|
+
export interface PluginPreviewProps<P extends Props = Props> {
|
|
80
|
+
props: P;
|
|
81
|
+
blocks: StoredBlock[];
|
|
82
|
+
plugins: BlockPlugin[];
|
|
83
|
+
}
|
|
84
|
+
/** A custom block for the slash menu. */
|
|
85
|
+
/** What a plugin's `markdown` gets beside its props. */
|
|
86
|
+
export interface MarkdownContext {
|
|
87
|
+
/** The nested blocks as markdown ('' when it has none). */
|
|
88
|
+
inner: string;
|
|
89
|
+
/** The media of the document, to turn an attachment id into its url. */
|
|
90
|
+
media: MediaItem[];
|
|
91
|
+
}
|
|
92
|
+
export interface BlockPlugin<P extends Props = Props> {
|
|
93
|
+
/** Stored `type`; must not start with 'md:'. */
|
|
94
|
+
type: string;
|
|
95
|
+
label: string;
|
|
96
|
+
hint: string;
|
|
97
|
+
keys: string;
|
|
98
|
+
icon: Icon;
|
|
99
|
+
/** Properties of a new block. */
|
|
100
|
+
props: () => P;
|
|
101
|
+
/** Its fields, on the top of the block. */
|
|
102
|
+
fields?: PropField[];
|
|
103
|
+
/** The mini label on the top of the block (before its fields). */
|
|
104
|
+
tag(props: P): string;
|
|
105
|
+
/** What the block holds; decides what "Turn into" can offer. 'blocks' gets a nested document. */
|
|
106
|
+
content: Content;
|
|
107
|
+
/** In the editor. */
|
|
108
|
+
view: Component<PluginViewProps<any>, {}, 'props' | 'blocks'>;
|
|
109
|
+
/**
|
|
110
|
+
* On the site: the block as it will be rendered by `<Preview>`. Optional: an app with its own renderer leaves it out,
|
|
111
|
+
* and `<Preview>` then skips the block.
|
|
112
|
+
*/
|
|
113
|
+
preview?: Component<PluginPreviewProps<any>>;
|
|
114
|
+
/**
|
|
115
|
+
* For a plugin with nested blocks: the blocks it takes, as palette ids ('md:p', 'image'…); 'md:*' stands for every
|
|
116
|
+
* built-in block. Its nested editor offers the host's palette narrowed to these. Default: everything the host has.
|
|
117
|
+
*/
|
|
118
|
+
accepts?: string[];
|
|
119
|
+
/**
|
|
120
|
+
* Its markdown form (copying as text, export); `inner` holds its nested blocks already as markdown. Without it, or
|
|
121
|
+
* when it gives '', the block is left out of the markdown.
|
|
122
|
+
*/
|
|
123
|
+
markdown?(props: P, ctx: MarkdownContext): string;
|
|
124
|
+
/** At most one per document, and only at the top level (e.g. the supporters-only divider). */
|
|
125
|
+
unique?: boolean;
|
|
126
|
+
/** The blocks after it are gated (e.g. for supporters only): the preview marks them. Implies nothing about `unique`. */
|
|
127
|
+
gate?: boolean;
|
|
128
|
+
}
|
|
129
|
+
export { MAX_INDENT } from './model';
|
|
130
|
+
export declare const uid: () => string;
|
|
131
|
+
export declare const newBlock: (type?: BlockType, html?: string, extra?: Partial<Block>) => Block;
|
|
132
|
+
/** Ordinal for every `ol` block, restarting after interruptions and per indent level. */
|
|
133
|
+
export declare function numbering(blocks: Block[]): Record<string, number>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The stored form is shared with the server (validation, search text, the HTML converter).
|
|
2
|
+
export { isMd, MD } from './model';
|
|
3
|
+
/** Content of the built-in block types. */
|
|
4
|
+
export const BUILT_IN_CONTENT = {
|
|
5
|
+
p: 'text',
|
|
6
|
+
h1: 'text',
|
|
7
|
+
h2: 'text',
|
|
8
|
+
h3: 'text',
|
|
9
|
+
h4: 'text',
|
|
10
|
+
h5: 'text',
|
|
11
|
+
h6: 'text',
|
|
12
|
+
ul: 'text',
|
|
13
|
+
ol: 'text',
|
|
14
|
+
todo: 'text',
|
|
15
|
+
quote: 'text',
|
|
16
|
+
code: 'plain',
|
|
17
|
+
hr: 'none'
|
|
18
|
+
};
|
|
19
|
+
export { MAX_INDENT } from './model';
|
|
20
|
+
export const uid = () => Math.random().toString(36).slice(2, 10);
|
|
21
|
+
export const newBlock = (type = 'p', html = '', extra = {}) => ({
|
|
22
|
+
id: uid(),
|
|
23
|
+
type,
|
|
24
|
+
html,
|
|
25
|
+
indent: 0,
|
|
26
|
+
...extra
|
|
27
|
+
});
|
|
28
|
+
/** Ordinal for every `ol` block, restarting after interruptions and per indent level. */
|
|
29
|
+
export function numbering(blocks) {
|
|
30
|
+
const out = {};
|
|
31
|
+
const counters = [];
|
|
32
|
+
for (const b of blocks) {
|
|
33
|
+
if (b.type === 'ol') {
|
|
34
|
+
counters.length = b.indent + 1;
|
|
35
|
+
counters[b.indent] = (counters[b.indent] ?? 0) + 1;
|
|
36
|
+
out[b.id] = counters[b.indent];
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
counters.length = Math.min(counters.length, b.indent);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
package/dist/view.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type Get = () => string | undefined;
|
|
2
|
+
/** Call in a component's init (around the editor); the function returns the current view's key. */
|
|
3
|
+
export declare function provideView(get: Get): void;
|
|
4
|
+
/** Call in a component's init; the returned function reads the current view's key (undefined: none given). */
|
|
5
|
+
export declare const useView: () => Get;
|
|
6
|
+
export {};
|
package/dist/view.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The view the per-view fields edit (see PropField.views). Which views there are and which one is shown is the
|
|
2
|
+
// host's business (e.g. the width its preview shows): it gives the current one through the context. Without it, a
|
|
3
|
+
// per-view field simply edits its base value.
|
|
4
|
+
import { getContext, setContext } from 'svelte';
|
|
5
|
+
const KEY = 'block-editor:view';
|
|
6
|
+
/** Call in a component's init (around the editor); the function returns the current view's key. */
|
|
7
|
+
export function provideView(get) {
|
|
8
|
+
setContext(KEY, get);
|
|
9
|
+
}
|
|
10
|
+
/** Call in a component's init; the returned function reads the current view's key (undefined: none given). */
|
|
11
|
+
export const useView = () => getContext(KEY) ?? (() => undefined);
|