@atom-forge/brickee 0.0.0-stage → 0.0.2
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 +42 -0
- package/LICENSE +21 -0
- package/README-AI.md +286 -0
- package/README.md +41 -2
- package/dist/Editor.svelte +1956 -0
- package/dist/Editor.svelte.d.ts +19 -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 +182 -0
- package/dist/Preview.svelte.d.ts +32 -0
- package/dist/PropsBar.svelte +405 -0
- package/dist/PropsBar.svelte.d.ts +17 -0
- package/dist/code.d.ts +12 -0
- package/dist/code.js +15 -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 +142 -0
- package/dist/markdown.d.ts +23 -0
- package/dist/markdown.js +219 -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 +537 -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 +105 -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 +132 -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 +114 -0
- package/docs/en/editor-and-preview.md +139 -0
- package/docs/en/getting-started.md +85 -0
- package/docs/en/host-contexts.md +169 -0
- package/docs/en/index.md +38 -0
- package/docs/en/recipes-and-pitfalls.md +123 -0
- package/docs/en/server-side.md +95 -0
- package/docs/en/upgrading.md +71 -0
- package/docs/en/writing-plugins.md +405 -0
- package/docs/hu/bundled-plugins.md +99 -0
- package/docs/hu/documents-and-markdown.md +114 -0
- package/docs/hu/editor-and-preview.md +139 -0
- package/docs/hu/getting-started.md +85 -0
- package/docs/hu/host-contexts.md +169 -0
- package/docs/hu/index.md +38 -0
- package/docs/hu/recipes-and-pitfalls.md +123 -0
- package/docs/hu/server-side.md +95 -0
- package/docs/hu/upgrading.md +71 -0
- package/docs/hu/writing-plugins.md +405 -0
- package/package.json +114 -5
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# Writing plugins
|
|
2
|
+
|
|
3
|
+
[← Previous](bundled-plugins.md) · [Contents](index.md) · [Next →](host-contexts.md)
|
|
4
|
+
|
|
5
|
+
A plugin is a `BlockPlugin` object plus two Svelte components: a **view** for the editor and a **preview** for the site. Everything a plugin needs is exported from `@atom-forge/brickee` — the bundled plugins use nothing else.
|
|
6
|
+
|
|
7
|
+
## Anatomy
|
|
8
|
+
|
|
9
|
+
| Member | Type | Meaning |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `type` | `string` | The stored `type`. Must not start with `md:`; to pass the schema it must match `^[a-z][a-z0-9:-]{0,39}$`. |
|
|
12
|
+
| `label`, `hint` | `string` | Name and short description in the slash and Turn-into menus. |
|
|
13
|
+
| `keys` | `string` | Extra search words for the slash menu. |
|
|
14
|
+
| `icon` | `Icon` | A `@lucide/svelte` icon component. |
|
|
15
|
+
| `props` | `() => P` | The props of a new block. |
|
|
16
|
+
| `fields` | `PropField[]` | The settings on the top of the block (optional). |
|
|
17
|
+
| `tag` | `(props: P) => string` | The small label on the top of the block, before its fields. Parts separated by ` · ` are translated one by one. |
|
|
18
|
+
| `content` | `'none' \| 'text' \| 'plain' \| 'blocks'` | What the block holds. Plugins use `'none'` (props only) or `'blocks'` (a nested document); it decides what Turn-into offers. |
|
|
19
|
+
| `view` | `Component<PluginViewProps>` | The block in the editor. |
|
|
20
|
+
| `preview` | `Component<PluginPreviewProps>` | The block on the site (optional). Without it, `Preview` renders nothing for the block — for apps that render stored blocks with their own renderer. |
|
|
21
|
+
| `accepts` | `string[]` | A container's allowed nested blocks, as palette ids; `'md:*'` is every built-in block. Default: everything the host offers. |
|
|
22
|
+
| `markdown` | `(props: P, ctx: MarkdownContext) => string` | Its markdown form (optional). |
|
|
23
|
+
| `unique` | `boolean` | At most one per document, top level only. |
|
|
24
|
+
| `gate` | `boolean` | The blocks after it are gated; the preview marks them. |
|
|
25
|
+
|
|
26
|
+
`P` is the props type of the plugin; declare it as an interface extending `Record<string, unknown>`.
|
|
27
|
+
|
|
28
|
+
## A plugin without content
|
|
29
|
+
|
|
30
|
+
A link button: a label, a link, a size and a colour — all set in fields, nothing typed into the block itself.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// safe-url.ts — only links the site should render
|
|
34
|
+
export const safeUrl = (u: unknown) => {
|
|
35
|
+
const s = String(u ?? '').trim();
|
|
36
|
+
return /^(https?:|mailto:|\/(?!\/))/.test(s) ? s : '';
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```svelte
|
|
41
|
+
<!-- ButtonView.svelte: the block in the editor -->
|
|
42
|
+
<script lang="ts" module>
|
|
43
|
+
export interface ButtonProps extends Record<string, unknown> {
|
|
44
|
+
label: string;
|
|
45
|
+
url: string;
|
|
46
|
+
size: 'normal' | 'large';
|
|
47
|
+
color: string;
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<script lang="ts">
|
|
52
|
+
import { MousePointerClick } from '@lucide/svelte';
|
|
53
|
+
import { Empty, safeColor, type PluginViewProps } from '@atom-forge/brickee';
|
|
54
|
+
|
|
55
|
+
// Both bindable, even when the view does not use them: the editor binds them on every plugin view.
|
|
56
|
+
let { props = $bindable(), blocks = $bindable() }: PluginViewProps<ButtonProps> = $props();
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
{#if props.label}
|
|
60
|
+
<span class="button" style:background={safeColor(props.color, '#2563eb')}>{props.label}</span>
|
|
61
|
+
{:else}
|
|
62
|
+
<Empty icon={MousePointerClick} text="Set the label and the link above" />
|
|
63
|
+
{/if}
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
.button {
|
|
67
|
+
display: inline-block;
|
|
68
|
+
padding: 0.5em 1.2em;
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
color: #fff;
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```svelte
|
|
76
|
+
<!-- ButtonPreview.svelte: the block on the site -->
|
|
77
|
+
<script lang="ts">
|
|
78
|
+
import { safeColor, type PluginPreviewProps } from '@atom-forge/brickee';
|
|
79
|
+
import type { ButtonProps } from './ButtonView.svelte';
|
|
80
|
+
import { safeUrl } from './safe-url';
|
|
81
|
+
|
|
82
|
+
let { props }: PluginPreviewProps<ButtonProps> = $props();
|
|
83
|
+
const href = $derived(safeUrl(props.url));
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
{#if href && props.label}
|
|
87
|
+
<p>
|
|
88
|
+
<a class="button" class:large={props.size === 'large'} {href} style:background={safeColor(props.color, '#2563eb')}>
|
|
89
|
+
{props.label}
|
|
90
|
+
</a>
|
|
91
|
+
</p>
|
|
92
|
+
{/if}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// button.ts: the plugin
|
|
97
|
+
import { MousePointerClick } from '@lucide/svelte';
|
|
98
|
+
import { mdText, type BlockPlugin } from '@atom-forge/brickee';
|
|
99
|
+
import ButtonView, { type ButtonProps } from './ButtonView.svelte';
|
|
100
|
+
import ButtonPreview from './ButtonPreview.svelte';
|
|
101
|
+
import { safeUrl } from './safe-url';
|
|
102
|
+
|
|
103
|
+
export const button: BlockPlugin<ButtonProps> = {
|
|
104
|
+
type: 'button',
|
|
105
|
+
label: 'Button',
|
|
106
|
+
hint: 'A link as a button',
|
|
107
|
+
keys: 'button link cta',
|
|
108
|
+
icon: MousePointerClick,
|
|
109
|
+
props: () => ({ label: '', url: '', size: 'normal', color: '#2563eb' }),
|
|
110
|
+
fields: [
|
|
111
|
+
{ key: 'label', label: 'Label', kind: 'text' },
|
|
112
|
+
{ key: 'url', label: 'Link', kind: 'text', placeholder: 'https://…', parse: (s) => s.trim() },
|
|
113
|
+
{
|
|
114
|
+
key: 'size',
|
|
115
|
+
label: 'Size',
|
|
116
|
+
kind: 'select',
|
|
117
|
+
options: [
|
|
118
|
+
{ value: 'normal', label: 'Normal' },
|
|
119
|
+
{ value: 'large', label: 'Large' }
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
{ key: 'color', label: 'Colour', kind: 'color' }
|
|
123
|
+
],
|
|
124
|
+
tag: (p) => (p.size === 'large' ? 'button · large' : 'button'),
|
|
125
|
+
content: 'none',
|
|
126
|
+
markdown: (p) => {
|
|
127
|
+
const href = safeUrl(p.url);
|
|
128
|
+
return href ? `[${mdText(p.label) || href}](${href})` : '';
|
|
129
|
+
},
|
|
130
|
+
view: ButtonView,
|
|
131
|
+
preview: ButtonPreview
|
|
132
|
+
};
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Register it beside the others:
|
|
136
|
+
|
|
137
|
+
```svelte
|
|
138
|
+
<Editor bind:value plugins={[...PLUGINS, button]} />
|
|
139
|
+
<Preview blocks={value} plugins={[...PLUGINS, button]} />
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
> **Props come from users**
|
|
143
|
+
>
|
|
144
|
+
> A plugin's props are whatever was stored — typed by an editor, pasted, or sent to your API by anyone who can save a document. The schema only checks the shape. Every preview must treat props as untrusted: take colours through `safeColor`, check links and image addresses, and clamp numbers. Svelte escapes text in `{...}`; never put a prop into `{@html}`.
|
|
145
|
+
|
|
146
|
+
## Fields
|
|
147
|
+
|
|
148
|
+
Each `PropField` is one setting on the top of the block. The editor draws them and stores their values in `props`.
|
|
149
|
+
|
|
150
|
+
| Member | Meaning |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `key` | The prop it edits. |
|
|
153
|
+
| `label` | Its label (translated through `messages`). |
|
|
154
|
+
| `kind` | `text`, `textarea`, `color`, `select`, `slider` or `toggles`. |
|
|
155
|
+
| `placeholder` | Hint of an empty text field; for an optional colour, the colour a click on "none" starts from. |
|
|
156
|
+
| `options` | `{ value, label }[]` for `select`, and for a `slider` that steps through values. An option label may use `{n}`, filled with its value. For `toggles`, `{ value, label, icon }[]`: `icon` is the button, `label` its tooltip. |
|
|
157
|
+
| `min`, `max`, `step`, `unit` | A number `slider`: its range, step (default 1) and the unit shown after the value. |
|
|
158
|
+
| `none` | The label of the unset state: a number slider's notch left of `min`, or an optional colour's "none" button. |
|
|
159
|
+
| `views` | Per-view values for a `slider` (see [Per-view values](host-contexts.md#per-view-values)). |
|
|
160
|
+
| `format` | The field's text from the stored value (default: the value as a string). |
|
|
161
|
+
| `parse` | The stored value from the field's text (default: the text). |
|
|
162
|
+
|
|
163
|
+
How each kind stores its value:
|
|
164
|
+
|
|
165
|
+
| Kind | Stored value |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `text`, `textarea` | The text, or what `parse` returns (`parse: lines` stores a list of lines). |
|
|
168
|
+
| `select` | The chosen option's `value`. |
|
|
169
|
+
| `slider` with `options` | The value of the option at the slider's position. |
|
|
170
|
+
| `slider` with `min` and `max` | The number as a string (for example `'56'`); the `none` notch removes the key. |
|
|
171
|
+
| `color` | A `#rrggbb` string; with `none`, clearing it removes the key. |
|
|
172
|
+
| `toggles` | The list of the options switched on, in `options` order (`[]` when none); the bundled table's header and footer are one. |
|
|
173
|
+
|
|
174
|
+
An empty value (`''`, `null` or `undefined`, also when returned by `parse`) removes the key from `props`, so a preview must always handle a missing prop. A field's `parse` is also a good place to normalize input: the bundled YouTube field accepts any YouTube link and stores only the video id.
|
|
175
|
+
|
|
176
|
+
## The view
|
|
177
|
+
|
|
178
|
+
The view receives `PluginViewProps`:
|
|
179
|
+
|
|
180
|
+
| Prop | Meaning |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `props` | The block's props (bindable): a view may also edit them in place, as the table does. |
|
|
183
|
+
| `blocks` | The nested document (bindable); always `[]` for a plugin without content. |
|
|
184
|
+
| `readonly` | The editor is read-only. |
|
|
185
|
+
| `palette`, `plugins` | The host's palette (already narrowed to `accepts`) and plugins, for a nested editor. |
|
|
186
|
+
|
|
187
|
+
Declare `props` and `blocks` with `$bindable()` even when the view does not use them. The fields are drawn above the view automatically; a view only has to show the block. `Empty` (props `icon` and `text`) is the dashed placeholder the bundled plugins show until their props are set; its text is translated.
|
|
188
|
+
|
|
189
|
+
## The preview
|
|
190
|
+
|
|
191
|
+
The preview receives `PluginPreviewProps`: `props`, `blocks` and `plugins`. It is a plain Svelte component that renders the block for the site. Messages and the view are available through the contexts (`useText`, `useView`; see [Host contexts](host-contexts.md)); images are the plugin's own business (see [Images](#images)).
|
|
192
|
+
|
|
193
|
+
## Containers
|
|
194
|
+
|
|
195
|
+
A plugin with `content: 'blocks'` holds a nested document in `blocks`. Its view uses `Frame`, a thin box that draws the block's fields on its top and a nested editor below; its preview renders the inner blocks with a nested `Preview`.
|
|
196
|
+
|
|
197
|
+
```svelte
|
|
198
|
+
<!-- NoteView.svelte -->
|
|
199
|
+
<script lang="ts" module>
|
|
200
|
+
export interface NoteProps extends Record<string, unknown> {
|
|
201
|
+
title: string;
|
|
202
|
+
}
|
|
203
|
+
</script>
|
|
204
|
+
|
|
205
|
+
<script lang="ts">
|
|
206
|
+
import { Frame, useText, type PluginViewProps } from '@atom-forge/brickee';
|
|
207
|
+
|
|
208
|
+
let { props = $bindable(), blocks = $bindable(), readonly, palette, plugins }: PluginViewProps<NoteProps> = $props();
|
|
209
|
+
const tr = useText();
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
<Frame bind:blocks {readonly} {palette} {plugins} heading={props.title} empty={tr('Note')} placeholder={tr('Write the note…')} />
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```svelte
|
|
216
|
+
<!-- NotePreview.svelte -->
|
|
217
|
+
<script lang="ts">
|
|
218
|
+
import { Preview, type PluginPreviewProps } from '@atom-forge/brickee';
|
|
219
|
+
import type { NoteProps } from './NoteView.svelte';
|
|
220
|
+
|
|
221
|
+
let { props, blocks, plugins }: PluginPreviewProps<NoteProps> = $props();
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<aside class="note">
|
|
225
|
+
{#if props.title}<p class="note-title">{props.title}</p>{/if}
|
|
226
|
+
<Preview {blocks} {plugins} nested />
|
|
227
|
+
</aside>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// note.ts
|
|
232
|
+
import { StickyNote } from '@lucide/svelte';
|
|
233
|
+
import { mdJoin, mdQuote, mdText, type BlockPlugin } from '@atom-forge/brickee';
|
|
234
|
+
import NoteView, { type NoteProps } from './NoteView.svelte';
|
|
235
|
+
import NotePreview from './NotePreview.svelte';
|
|
236
|
+
|
|
237
|
+
export const note: BlockPlugin<NoteProps> = {
|
|
238
|
+
type: 'note',
|
|
239
|
+
label: 'Note',
|
|
240
|
+
hint: 'Boxed text with a title',
|
|
241
|
+
keys: 'note aside box',
|
|
242
|
+
icon: StickyNote,
|
|
243
|
+
props: () => ({ title: '' }),
|
|
244
|
+
fields: [{ key: 'title', label: 'Title', kind: 'text' }],
|
|
245
|
+
tag: () => 'note',
|
|
246
|
+
content: 'blocks',
|
|
247
|
+
accepts: ['md:p', 'md:ul', 'md:ol', 'youtube'],
|
|
248
|
+
markdown: (p, { inner }) => mdQuote(mdJoin(p.title && `**${mdText(p.title)}**`, inner)),
|
|
249
|
+
view: NoteView,
|
|
250
|
+
preview: NotePreview
|
|
251
|
+
};
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`Frame` props: `blocks` (bindable), `readonly`, `palette`, `plugins`, and optionally `placeholder` for the nested editor. `heading`, `empty` and `image` (a URL) make a heading line, which is shown only when the fields are not (read-only mode or no fields).
|
|
255
|
+
|
|
256
|
+
How nesting works:
|
|
257
|
+
|
|
258
|
+
- **One level.** A nested editor never offers blocks that hold blocks themselves, nor unique blocks. The schema rejects deeper documents.
|
|
259
|
+
- **`accepts`** narrows what the nested editor offers to the listed palette ids, in the host's order. `'md:*'` stands for every built-in block, so `['md:*', 'youtube']` is "text and videos". Without `accepts`, the nested editor offers everything the host does (minus containers and unique blocks).
|
|
260
|
+
- The host's `palette` still applies: `accepts` can only narrow it, never add to it.
|
|
261
|
+
- Wrapping blocks into a container from the Turn-into menu, and turning a container into another, is offered only when the container accepts every block going in.
|
|
262
|
+
- `blocksSchemaFor(plugins)` checks `accepts` on the server (see [On the server](server-side.md)).
|
|
263
|
+
|
|
264
|
+
## Unique and gate blocks
|
|
265
|
+
|
|
266
|
+
- **`unique: true`** — at most one per document, at the top level. Once the document has one, the plugin leaves the slash and Turn-into menus; nested editors never offer it.
|
|
267
|
+
- **`gate: true`** — the blocks after the first top-level gate block are gated. The `Preview` renders them inside a section marked "For supporters only" with a dashed line; `markGated={false}` turns the marking off. Nested previews never split.
|
|
268
|
+
|
|
269
|
+
A supporters-only divider is a typical example of both: one per document, with the gated content behind it.
|
|
270
|
+
|
|
271
|
+
> **Gating only marks**
|
|
272
|
+
>
|
|
273
|
+
> `gate` does not hide anything: the preview still renders every block, and the stored document still holds them. Deciding who may read the gated part — and leaving it out of the page for everyone else — is the host's job, on the server (see [Gated content](server-side.md#gated-content)). `unique` is not enforced by the schema either.
|
|
274
|
+
|
|
275
|
+
## Markdown form
|
|
276
|
+
|
|
277
|
+
`markdown(props, { inner })` returns the block as markdown, used when blocks are copied in the editor and by `toMarkdown`. `inner` is the nested blocks already as markdown (`''` without any).
|
|
278
|
+
|
|
279
|
+
Return `''` to leave the block out. Plugins whose output has no meaning as text (an ad slot, the bundled `gap`) simply have no markdown form. Three helpers keep the output well-formed:
|
|
280
|
+
|
|
281
|
+
| Helper | Does |
|
|
282
|
+
|---|---|
|
|
283
|
+
| `mdText(value)` | Plain text made safe inside markdown: escapes inline marks and `#`, joins lines. |
|
|
284
|
+
| `mdJoin(...parts)` | Joins parts as separate blocks (blank line between), leaving out empty and falsy ones. |
|
|
285
|
+
| `mdQuote(md)` | Prefixes every line with `> `, for a box rendered as a blockquote. |
|
|
286
|
+
|
|
287
|
+
## Images
|
|
288
|
+
|
|
289
|
+
The editor has no notion of images: no image field, no picker, no list of media. Whether a block stores an image's URL or the id of an image in the app's own media library, how the user picks it, and how the stored value becomes an address are the application's decisions, so an image block is an ordinary plugin of the app.
|
|
290
|
+
|
|
291
|
+
A minimal `photo` plugin stores the image's address, typed into a text field, with alt text and a height slider:
|
|
292
|
+
|
|
293
|
+
```svelte
|
|
294
|
+
<!-- PhotoView.svelte -->
|
|
295
|
+
<script lang="ts" module>
|
|
296
|
+
export interface PhotoProps extends Record<string, unknown> {
|
|
297
|
+
/** The image's address. */
|
|
298
|
+
url: string;
|
|
299
|
+
alt: string;
|
|
300
|
+
/** The height in % of the width (20–150), or unset for the image's own shape. */
|
|
301
|
+
ratio?: unknown;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The stored address if it is http(s) or site-relative, else ''. */
|
|
305
|
+
export const imageSrc = (u: unknown) => {
|
|
306
|
+
const s = String(u ?? '').trim();
|
|
307
|
+
return /^(https?:|\/(?!\/))/.test(s) ? s : '';
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/** A CSS aspect-ratio from a stored height, or undefined. */
|
|
311
|
+
export const aspect = (v: unknown) => {
|
|
312
|
+
const n = Number(v);
|
|
313
|
+
return n >= 20 && n <= 150 ? `100 / ${n}` : undefined;
|
|
314
|
+
};
|
|
315
|
+
</script>
|
|
316
|
+
|
|
317
|
+
<script lang="ts">
|
|
318
|
+
import { ImageIcon } from '@lucide/svelte';
|
|
319
|
+
import { Empty, type PluginViewProps } from '@atom-forge/brickee';
|
|
320
|
+
|
|
321
|
+
let { props = $bindable(), blocks = $bindable() }: PluginViewProps<PhotoProps> = $props();
|
|
322
|
+
const src = $derived(imageSrc(props.url));
|
|
323
|
+
</script>
|
|
324
|
+
|
|
325
|
+
{#if src}
|
|
326
|
+
<img {src} alt={props.alt} style="max-width: 100%" />
|
|
327
|
+
{:else}
|
|
328
|
+
<Empty icon={ImageIcon} text="Set the image's address above" />
|
|
329
|
+
{/if}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
```svelte
|
|
333
|
+
<!-- PhotoPreview.svelte -->
|
|
334
|
+
<script lang="ts">
|
|
335
|
+
import { baseValue, type PluginPreviewProps } from '@atom-forge/brickee';
|
|
336
|
+
import { aspect, imageSrc, type PhotoProps } from './PhotoView.svelte';
|
|
337
|
+
|
|
338
|
+
let { props }: PluginPreviewProps<PhotoProps> = $props();
|
|
339
|
+
const src = $derived(imageSrc(props.url));
|
|
340
|
+
</script>
|
|
341
|
+
|
|
342
|
+
{#if src}
|
|
343
|
+
<img {src} alt={props.alt ?? ''} style:aspect-ratio={aspect(baseValue(props.ratio))} />
|
|
344
|
+
{/if}
|
|
345
|
+
|
|
346
|
+
<style>
|
|
347
|
+
img {
|
|
348
|
+
width: 100%;
|
|
349
|
+
object-fit: cover;
|
|
350
|
+
}
|
|
351
|
+
</style>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
// photo.ts
|
|
356
|
+
import { ImageIcon } from '@lucide/svelte';
|
|
357
|
+
import { mdText, type BlockPlugin } from '@atom-forge/brickee';
|
|
358
|
+
import PhotoView, { type PhotoProps } from './PhotoView.svelte';
|
|
359
|
+
import PhotoPreview from './PhotoPreview.svelte';
|
|
360
|
+
|
|
361
|
+
export const photo: BlockPlugin<PhotoProps> = {
|
|
362
|
+
type: 'photo',
|
|
363
|
+
label: 'Photo',
|
|
364
|
+
hint: 'One image',
|
|
365
|
+
keys: 'photo image picture',
|
|
366
|
+
icon: ImageIcon,
|
|
367
|
+
props: () => ({ url: '', alt: '' }),
|
|
368
|
+
fields: [
|
|
369
|
+
{ key: 'url', label: 'Image', kind: 'text', placeholder: 'https://…', parse: (s) => s.trim() },
|
|
370
|
+
{ key: 'alt', label: 'Alt text', kind: 'text', placeholder: 'What the image shows' },
|
|
371
|
+
{ key: 'ratio', label: 'Height', kind: 'slider', min: 20, max: 150, unit: '%', none: 'Original' }
|
|
372
|
+
],
|
|
373
|
+
tag: () => 'photo',
|
|
374
|
+
content: 'none',
|
|
375
|
+
markdown: (p) => {
|
|
376
|
+
const src = imageSrc(p.url);
|
|
377
|
+
return src ? `` : '';
|
|
378
|
+
},
|
|
379
|
+
view: PhotoView,
|
|
380
|
+
preview: PhotoPreview
|
|
381
|
+
};
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The address is a prop like any other, so it is untrusted: the view and the preview render it only when it is an `http(s)` or site-relative address, as the button checks its link.
|
|
385
|
+
|
|
386
|
+
Other apps choose differently, all within the plugin:
|
|
387
|
+
|
|
388
|
+
- **A media library.** The block stores the id of an image in the app's library instead of a URL. The view, the preview and `markdown` turn the id into an address with the app's own lookup: a module of the app, or the app's own Svelte context provided above the editor and the preview (plugin components are rendered beneath them). `markdown` runs outside components, when blocks are copied and in `toMarkdown`, so it can only use module data.
|
|
389
|
+
- **Picking.** Instead of a text field, the view can show its own picker — a button that opens the app's media dialog or an upload — and write the result into `props`, which is bindable.
|
|
390
|
+
- **Several images.** A gallery stores a list (of URLs or ids) in one prop and edits it in its view the same way.
|
|
391
|
+
|
|
392
|
+
## The kit
|
|
393
|
+
|
|
394
|
+
| Export | Kind | Use |
|
|
395
|
+
|---|---|---|
|
|
396
|
+
| `Frame` | component | The editor look of a container: fields on top, nested editor below. |
|
|
397
|
+
| `Empty` | component | The placeholder of a block whose props are not set yet. |
|
|
398
|
+
| `safeColor(value, fallback)` | function | A `#hex` colour (3–8 digits), or the fallback. |
|
|
399
|
+
| `lines(text)` | function | Trimmed, non-empty lines: a field's `parse` for "one per line" lists. |
|
|
400
|
+
| `baseValue(value)` | function | The base value of a per-view prop. |
|
|
401
|
+
| `viewValues(value, keys)` | function | A per-view prop's value for each view. |
|
|
402
|
+
| `mdText`, `mdJoin`, `mdQuote` | functions | Markdown forms (above). |
|
|
403
|
+
| `useText` | function | Translates the plugin's own UI text. |
|
|
404
|
+
| `useView` | function | The view the host is showing. |
|
|
405
|
+
| `Preview` | component | Renders a container's nested blocks (with `nested`). |
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Mellékelt pluginok
|
|
2
|
+
|
|
3
|
+
[← Előző](documents-and-markdown.md) · [Tartalom](index.md) · [Következő →](writing-plugins.md)
|
|
4
|
+
|
|
5
|
+
Az `@atom-forge/brickee/plugins` négy általános célú plugint tartalmaz. Kizárólag azzal készültek, amit a mag bármely plugin-szerzőnek exportál, így példaként is szolgálnak, és csak azokat veszed át, amelyekre szükséged van.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { PLUGINS } from '@atom-forge/brickee/plugins'; // mind a négy
|
|
9
|
+
import { table, youtube } from '@atom-forge/brickee/plugins'; // vagy néhány
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## A pluginok
|
|
13
|
+
|
|
14
|
+
| Export | `type` | Mezők | Markdown alak | Mi ez |
|
|
15
|
+
|---|---|---|---|---|
|
|
16
|
+
| `table` | `table` | fejléc és lábléc (a cellák helyben szerkeszthetők) | GFM-táblázat | Inline formázott cellák sorai és oszlopai; egy cellába kattintva megjelennek a sora és oszlopa eszközei (beszúrás, törlés, igazítás), a sorokat és az oszlopokat a fogantyújuknál fogva húzhatod. Az első sor lehet fejléc, az utolsó lábléc. |
|
|
17
|
+
| `youtube` | `youtube` | videóhivatkozás vagy -azonosító, cím | hivatkozás | YouTube-videó, kattintásra betöltve; bármely YouTube-hivatkozásból csak a videó azonosítója marad meg. |
|
|
18
|
+
| `map` | `map` | hely vagy cím, nagyításcsúszka | hivatkozás | Egy hely vagy cím beágyazott Google Maps-térképe. |
|
|
19
|
+
| `gap` | `gap` | magasságcsúszka | nincs | Üres hely a blokkok között, 1–8 sor magas. |
|
|
20
|
+
|
|
21
|
+
Egyikük sem tartalmaz beágyazott blokkokat. A képblokkok, a dobozok és a többi tároló megjelenése és viselkedése alkalmazásonként más, ezért a csomag ezeket rád hagyja: a [Pluginok írása](writing-plugins.md) egy tárolót mutat be, [Az alkalmazás kontextusai](host-contexts.md) pedig egy képblokkot.
|
|
22
|
+
|
|
23
|
+
A pluginok mellett a belépési pont exportálja a props-típusaikat (`TableProps`, `YouTubeProps`, `MapProps`, `GapProps`) és az előnézeteik által használt segédfüggvényeket, amelyek saját előnézetek írásakor jól jönnek: `youtubeId` (videóazonosító bármely YouTube-hivatkozásból), `mapSrc` (egy hely beágyazási címe), `gapLines`, `GAP_LINES` és `GAP_MAX`, valamint a táblázatokhoz `tableHeads`, `rowHead`, `HEADS` és `square`.
|
|
24
|
+
|
|
25
|
+
## Előnézet felülírása
|
|
26
|
+
|
|
27
|
+
A plugin egyszerű objektum, így az átalakítása egy spread. A leggyakoribb változtatás az előnézet: a szerkesztőnézet a csomagé marad, az oldal pedig a saját komponenseddel rendereli a blokkot – így a szerkesztő előnézete és a közzétett oldal pontosan ugyanaz.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// blocks.ts
|
|
31
|
+
import type { BlockPlugin } from '@atom-forge/brickee';
|
|
32
|
+
import { gap, table, youtube } from '@atom-forge/brickee/plugins';
|
|
33
|
+
import SiteYouTube from './SiteYouTube.svelte';
|
|
34
|
+
|
|
35
|
+
const withPreview = (plugin: BlockPlugin, preview: BlockPlugin['preview']): BlockPlugin => ({ ...plugin, preview });
|
|
36
|
+
|
|
37
|
+
export const plugins: BlockPlugin[] = [withPreview(youtube, SiteYouTube), table, gap];
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A helyettesítő előnézet ugyanazokat a propsokat kapja, mint az eredeti (`PluginPreviewProps`: `props`, `blocks`, `plugins`). Ez lejátszó beágyazása helyett egy bélyegképet mutat, amely a videóra hivatkozik:
|
|
41
|
+
|
|
42
|
+
```svelte
|
|
43
|
+
<!-- SiteYouTube.svelte -->
|
|
44
|
+
<script lang="ts">
|
|
45
|
+
import type { PluginPreviewProps } from '@atom-forge/brickee';
|
|
46
|
+
import { youtubeId, type YouTubeProps } from '@atom-forge/brickee/plugins';
|
|
47
|
+
|
|
48
|
+
let { props }: PluginPreviewProps<YouTubeProps> = $props();
|
|
49
|
+
// a props felhasználói bemenet: csak a jól formált videóazonosítót tartsd meg
|
|
50
|
+
const id = $derived(youtubeId(String(props.id ?? '')));
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
{#if id.length === 11}
|
|
54
|
+
<a class="video" href="https://www.youtube.com/watch?v={id}" target="_blank" rel="noopener noreferrer">
|
|
55
|
+
<img src="https://img.youtube.com/vi/{id}/hqdefault.jpg" alt={props.title || 'YouTube video'} />
|
|
56
|
+
</a>
|
|
57
|
+
{/if}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Mezők hozzáadása
|
|
61
|
+
|
|
62
|
+
Az új beállítások új mezőket és új kezdő propsokat jelentenek. A plugin nézetén nem kell változtatni: a szerkesztő minden mezőt kirajzol a blokk tetején, az előnézeted pedig beolvassa az új propsokat.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import type { BlockPlugin } from '@atom-forge/brickee';
|
|
66
|
+
import { map } from '@atom-forge/brickee/plugins';
|
|
67
|
+
import SiteMap from './SiteMap.svelte';
|
|
68
|
+
|
|
69
|
+
export const siteMap: BlockPlugin = {
|
|
70
|
+
...map,
|
|
71
|
+
props: () => ({ ...map.props(), caption: '' }),
|
|
72
|
+
fields: [...(map.fields ?? []), { key: 'caption', label: 'Caption', kind: 'text' }],
|
|
73
|
+
preview: SiteMap
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```svelte
|
|
78
|
+
<!-- SiteMap.svelte -->
|
|
79
|
+
<script lang="ts">
|
|
80
|
+
import type { PluginPreviewProps } from '@atom-forge/brickee';
|
|
81
|
+
import { mapSrc, type MapProps } from '@atom-forge/brickee/plugins';
|
|
82
|
+
|
|
83
|
+
let { props }: PluginPreviewProps<MapProps> = $props();
|
|
84
|
+
const src = $derived(mapSrc(props));
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
{#if src}
|
|
88
|
+
<figure>
|
|
89
|
+
<iframe title={props.q} {src} loading="lazy"></iframe>
|
|
90
|
+
{#if props.caption}<figcaption>{props.caption}</figcaption>{/if}
|
|
91
|
+
</figure>
|
|
92
|
+
{/if}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Ha a szerkesztő nem angol nyelvű, add hozzá az új mezőcímkéket a `messages`-hez (lásd [Felületi szövegek](host-contexts.md#felületi-szövegek)). Ha egy meglévő csúszkát nézetenkéntivé akarsz tenni, képezd le a `fields` tömböt, és adj hozzá `views`-t (lásd [Nézetenkénti értékek](host-contexts.md#nézetenkénti-értékek)).
|
|
96
|
+
|
|
97
|
+
> **Tartsd meg a típust**
|
|
98
|
+
>
|
|
99
|
+
> Felülíráskor tartsd meg a plugin `type` értékét: a tárolt dokumentumok típus szerint hivatkoznak a blokkokra. Egy új `type` új blokkot jelent, és a meglévő dokumentumok nem fogják megtalálni a blokkjaikat.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Dokumentumok és markdown
|
|
2
|
+
|
|
3
|
+
[← Előző](editor-and-preview.md) · [Tartalom](index.md) · [Következő →](bundled-plugins.md)
|
|
4
|
+
|
|
5
|
+
## A tárolt alak
|
|
6
|
+
|
|
7
|
+
A dokumentum `StoredBlock` objektumok tömbje: egyszerű JSON, amelyet úgy, ahogy van, elmenthetsz egy adatbázis-oszlopba:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
interface StoredBlock {
|
|
11
|
+
type: string; // beépített blokkoknál 'md:p', 'md:h2'…; plugin-blokkoknál a plugin típusa
|
|
12
|
+
indent: number; // 0–6
|
|
13
|
+
text?: string; // beépített blokkok: inline markdown (md:code esetén nyers szöveg)
|
|
14
|
+
props?: Record<string, unknown>; // plugin-beállítások; md:todo { checked }, md:code { lang }
|
|
15
|
+
blocks?: StoredBlock[]; // beágyazott blokkokat tartalmazó pluginok: a belső dokumentumuk
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Egy rövid dokumentum egy bekezdéssel, egy listával, egy videóval és egy dobozzal (a [Pluginok írása](writing-plugins.md#tárolók) `note` tárolójával):
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
[
|
|
23
|
+
{ "type": "md:h2", "indent": 0, "text": "Getting there" },
|
|
24
|
+
{ "type": "md:p", "indent": 0, "text": "Take the **night train** or see the [timetable](https://example.com/trains)." },
|
|
25
|
+
{ "type": "md:ul", "indent": 0, "text": "Tickets online" },
|
|
26
|
+
{ "type": "md:ul", "indent": 1, "text": "Students get *20%* off" },
|
|
27
|
+
{ "type": "youtube", "indent": 0, "props": { "id": "aqz-KE-bpKQ", "title": "The route by night" } },
|
|
28
|
+
{
|
|
29
|
+
"type": "note",
|
|
30
|
+
"indent": 0,
|
|
31
|
+
"props": { "title": "Good to know" },
|
|
32
|
+
"blocks": [{ "type": "md:p", "indent": 0, "text": "Trains are cancelled on public holidays." }]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- A beépített típusok `md:` előtagot kapnak (`MD` konstans, `isMd(block)` vizsgálat); minden más plugin-típus.
|
|
38
|
+
- A plugin-blokkok csak a `type`, az `indent`, a `props` és – tárolóknál – a `blocks` mezőt őrzik meg. Hogy a props mit jelent, az a pluginon múlik.
|
|
39
|
+
- A beágyazás egy szint mély: egy dobozon belüli blokk maga már nem tartalmazhat blokkokat (a `MAX_DEPTH` értéke 2).
|
|
40
|
+
- A szerkesztő saját munkaalakja (`Block`, `id`-vel és inline HTML-lel) soha nem kerül tárolásra; a kettő között a `toStored` és a `fromStored` alakít át.
|
|
41
|
+
|
|
42
|
+
## Beépített blokkok
|
|
43
|
+
|
|
44
|
+
| Típus | Tárolt szöveg | Props | Markdown |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| `md:p` | inline markdown | — | maga a szöveg |
|
|
47
|
+
| `md:h1` … `md:h6` | inline markdown | — | `#` … `######` |
|
|
48
|
+
| `md:ul` | inline markdown | — | `- item` |
|
|
49
|
+
| `md:ol` | inline markdown | — | `1. item` (sorozatonként és behúzási szintenként számozva) |
|
|
50
|
+
| `md:todo` | inline markdown | `{ checked: boolean }` | `- [x] item` |
|
|
51
|
+
| `md:quote` | inline markdown | — | `> quote` |
|
|
52
|
+
| `md:code` | nyers szöveg | `{ lang?: string }` | kerített blokk a nyelvvel |
|
|
53
|
+
| `md:hr` | — | — | `---` |
|
|
54
|
+
|
|
55
|
+
Az `indent` egymásba ágyazza a listaelemeket (az egymást követő lista-blokkokból az előnézetben helyesen egymásba ágyazott `ul`/`ol` elemek lesznek), a bekezdéseket és az idézeteket pedig behúzza. A címsorok, a kódblokkok és az elválasztók figyelmen kívül hagyják.
|
|
56
|
+
|
|
57
|
+
### Inline markdown
|
|
58
|
+
|
|
59
|
+
A beépített blokkok szövege a markdown egy kicsi, biztonságos részhalmaza:
|
|
60
|
+
|
|
61
|
+
| Jelölés | Tárolt alak |
|
|
62
|
+
|---|---|
|
|
63
|
+
| félkövér | `**text**` |
|
|
64
|
+
| dőlt | `*text*` |
|
|
65
|
+
| aláhúzott, alsó index, felső index | `<u>text</u>`, `<sub>text</sub>`, `<sup>text</sup>` |
|
|
66
|
+
| áthúzott | `~~text~~` |
|
|
67
|
+
| kód | `` `text` `` |
|
|
68
|
+
| hivatkozás | `[text](url)` – csak a `http:`, `https:`, `mailto:`, `tel:` és a webhelyen belüli `/…` célok lesznek hivatkozások |
|
|
69
|
+
| sortörés | egy újsor karakter |
|
|
70
|
+
|
|
71
|
+
Az egyszerű szövegben a speciális karakterek visszaperjeles escape-elést kapnak, így soha nem válnak jelöléssé. Az `inline(md)` az inline markdownt HTML-lé alakítja: előbb escape-eli a szöveget, és csak utána teszi hozzá a fenti tageket, így a kimenete biztonságosan megjeleníthető `{@html}`-lel – az előnézet is így tesz.
|
|
72
|
+
|
|
73
|
+
## Markdown be és ki
|
|
74
|
+
|
|
75
|
+
A markdown teljes értékű formátum: a vágólap ezt hordozza, és a befogadó alkalmazás importálhatja és exportálhatja.
|
|
76
|
+
|
|
77
|
+
| Függvény | Miből → mibe | Hol fut |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `fromStored(block)` | `StoredBlock` → `Block` | bárhol |
|
|
80
|
+
| `toStored(block)` | `Block` → `StoredBlock` | csak böngészőben |
|
|
81
|
+
| `fromMarkdown(text)` | markdown → `Block[]` | bárhol |
|
|
82
|
+
| `toMarkdown(blocks, { plugins })` | `Block[]` → markdown | csak böngészőben |
|
|
83
|
+
| `inline(md)` | inline markdown → inline HTML | bárhol |
|
|
84
|
+
|
|
85
|
+
A markdown-függvények a szerkesztő alakján (`Block[]`) dolgoznak, ezért a tárolt dokumentumok a `fromStored` és a `toStored` függvényen mennek át:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { fromMarkdown, fromStored, toMarkdown, toStored, type StoredBlock } from '@atom-forge/brickee';
|
|
89
|
+
import { PLUGINS } from '@atom-forge/brickee/plugins';
|
|
90
|
+
|
|
91
|
+
// Tárolt dokumentum -> markdown (csak böngészőben).
|
|
92
|
+
export const exportMarkdown = (doc: StoredBlock[]) => toMarkdown(doc.map(fromStored), { plugins: PLUGINS });
|
|
93
|
+
|
|
94
|
+
// Markdown -> tárolt dokumentum (csak böngészőben).
|
|
95
|
+
export const importMarkdown = (text: string): StoredBlock[] => fromMarkdown(text).map(toStored);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> **Csak böngészőben**
|
|
99
|
+
>
|
|
100
|
+
> A `toMarkdown` és a `toStored` a DOM-on keresztül olvassa a szerkesztő inline HTML-jét, ezért szüksége van a `document`-re. A böngészőben hívd őket (eseménykezelőben, effectben), ne szerveroldali renderelés közben. Szerveroldali szöveghez a `blocksToPlainText`-et használd (lásd [A szerveren](server-side.md)).
|
|
101
|
+
|
|
102
|
+
Amit a `fromMarkdown` beolvas:
|
|
103
|
+
|
|
104
|
+
- címsorok, felsorolás-, számozott és teendő-elemek, idézetek, kerített kódblokkok a nyelvükkel, elválasztók és a fenti inline jelölések;
|
|
105
|
+
- a sor eleji szóközök behúzásként (szintenként két szóköz vagy egy tabulátor);
|
|
106
|
+
- minden nem üres sor külön blokként – a tördelt sorokat nem vonja össze egy bekezdéssé, és csak beépített blokkokat hoz létre (táblázatot, képet vagy plugin-blokkot nem).
|
|
107
|
+
|
|
108
|
+
Amit a `toMarkdown` kiír:
|
|
109
|
+
|
|
110
|
+
- az egymást követő listaelemeket egymást követő sorokba, minden más blokkot üres sorral elválasztva;
|
|
111
|
+
- a listaelemeket és a bekezdéseket szintenként két szóközzel behúzva;
|
|
112
|
+
- a plugin-blokkot a pluginja `markdown(props, { inner })` függvényével, ahol az `inner` a már átalakított beágyazott blokkjai; a markdown alak nélküli plugin, illetve az, amelyik `''`-t ad vissza, kimarad.
|
|
113
|
+
|
|
114
|
+
Saját pluginod markdown alakjának megírásáról a [Pluginok írása](writing-plugins.md#markdown-alak) fejezet szól.
|