@plutocms/docs 0.0.0-stage → 0.16.0

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 (65) hide show
  1. package/.gitattributes +1 -0
  2. package/.prettierignore +5 -0
  3. package/CHANGELOG.md +186 -0
  4. package/FEATURES.md +34 -0
  5. package/README.md +40 -2
  6. package/app/assets/css/pluto-layer.css +40 -0
  7. package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +165 -0
  8. package/app/components/Pluto/Docs/Blocks/PlutoBlockInspector.vue +519 -0
  9. package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +161 -0
  10. package/app/components/Pluto/Docs/Blocks/PlutoBlockModeSwitch.vue +46 -0
  11. package/app/components/Pluto/Docs/Blocks/PlutoBlockOutline.vue +146 -0
  12. package/app/components/Pluto/Docs/Blocks/PlutoBlockOutlineList.vue +347 -0
  13. package/app/components/Pluto/Docs/Blocks/PlutoBlockPalette.vue +86 -0
  14. package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +232 -0
  15. package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +269 -0
  16. package/app/components/Pluto/Docs/Blocks/PlutoBlockRawEditor.vue +62 -0
  17. package/app/components/Pluto/Docs/Blocks/PlutoBlockTextEditor.vue +92 -0
  18. package/app/components/Pluto/Docs/Blocks/PlutoPreviewBlock.vue +80 -0
  19. package/app/components/Pluto/Docs/PlutoFieldMdc.vue +117 -0
  20. package/app/components/Pluto/Docs/PlutoMdcInsertMenu.vue +36 -0
  21. package/app/components/PlutoContentPreview.vue +35 -0
  22. package/app/composables/pluto-block-editor.ts +459 -0
  23. package/app/composables/pluto-content-edit-link.ts +19 -0
  24. package/app/composables/pluto-content-preview.ts +29 -0
  25. package/app/composables/pluto-content-runtime.ts +237 -0
  26. package/app/pages/_pluto/preview.vue +555 -0
  27. package/app/plugins/pluto-docs.ts +50 -0
  28. package/i18n/locales/en.json +253 -0
  29. package/i18n/locales/pt-BR.json +289 -0
  30. package/modules/pluto-docs/blocks.ts +217 -0
  31. package/modules/pluto-docs/index.ts +230 -0
  32. package/modules/pluto-docs/manifest.ts +159 -0
  33. package/modules/pluto-docs/options.ts +258 -0
  34. package/nuxt.config.ts +36 -0
  35. package/package.json +56 -4
  36. package/server/handlers/navigation.get.ts +7 -0
  37. package/server/handlers/page.get.ts +7 -0
  38. package/server/handlers/surround.get.ts +7 -0
  39. package/server/plugins/pluto-docs.ts +25 -0
  40. package/server/utils/pluto-docs-runtime.ts +504 -0
  41. package/shared/types/docs.d.ts +40 -0
  42. package/shared/types/pluto-blocks.d.ts +10 -0
  43. package/shared/types/pluto-manifest.d.ts +6 -0
  44. package/shared/utils/blocks/component-meta.ts +285 -0
  45. package/shared/utils/blocks/define.ts +15 -0
  46. package/shared/utils/blocks/editor.ts +357 -0
  47. package/shared/utils/blocks/history.ts +94 -0
  48. package/shared/utils/blocks/mdc-props.ts +321 -0
  49. package/shared/utils/blocks/operations.ts +692 -0
  50. package/shared/utils/blocks/parse.ts +444 -0
  51. package/shared/utils/blocks/presets.ts +384 -0
  52. package/shared/utils/blocks/preview-marks.ts +98 -0
  53. package/shared/utils/blocks/preview-protocol.ts +195 -0
  54. package/shared/utils/blocks/registry.ts +295 -0
  55. package/shared/utils/blocks/serialize.ts +107 -0
  56. package/shared/utils/blocks/tree.ts +85 -0
  57. package/shared/utils/blocks/types.ts +195 -0
  58. package/shared/utils/content-route.ts +40 -0
  59. package/shared/utils/docs-type.ts +290 -0
  60. package/shared/utils/mdc-snippets.ts +114 -0
  61. package/shared/utils/navigation-config.ts +19 -0
  62. package/shared/utils/runtime-content.ts +403 -0
  63. package/tailwind.lint.css +10 -0
  64. package/tsconfig.json +18 -0
  65. package/vitest.config.ts +7 -0
@@ -0,0 +1,295 @@
1
+ // Join the presets, the component layer, the site overrides (`plutoBlocks` in
2
+ // app.config.ts) and the props found in the document into one definition for
3
+ // each component. Pure functions: the caller reads the app config and passes
4
+ // the sources in.
5
+
6
+ import type {
7
+ PlutoBlock,
8
+ PlutoBlockDefinition,
9
+ PlutoBlockOverride,
10
+ PlutoBlockPatch,
11
+ PlutoBlockPropDefinition,
12
+ PlutoBlocksConfig,
13
+ PlutoBlockSlotDefinition,
14
+ PlutoBlockSources,
15
+ PlutoComponentBlock,
16
+ } from './types'
17
+ import { BLOCK_GROUPS, BLOCK_PRESETS, HIDDEN_PROP_NAMES } from './presets'
18
+
19
+ // The typed identity helper is in its own file, so a component file that auto-imports it does not load the registry.
20
+ export { definePlutoBlock } from './define'
21
+
22
+ export interface ResolveOptions {
23
+ /** The layers: the component layer and the `plutoBlocks` value of the app config. */
24
+ sources?: PlutoBlockSources
25
+ /** A block of the doc. Its props and slots fill what the definition does not list. */
26
+ block?: PlutoComponentBlock
27
+ }
28
+
29
+ /** A readable name from a component name: `pluto-hero` gives `Pluto hero`, `starCount` gives `Star count`. */
30
+ export function humanizeName(name: string): string {
31
+ const text = name
32
+ .replace(/^u-/, '')
33
+ .replace(
34
+ /([a-z0-9])([A-Z])/g,
35
+ (_, before: string, letter: string) => `${before} ${letter.toLowerCase()}`
36
+ )
37
+ .replace(/[-_]+/g, ' ')
38
+ .trim()
39
+ return text ? text[0]!.toUpperCase() + text.slice(1) : name
40
+ }
41
+
42
+ /**
43
+ * The key of a component in the sources: the kebab name that `pascalCase` turns back into the
44
+ * component name. `PlutoHero` gives `pluto-hero`, and `UPageCTA` gives `u-page-c-t-a`. A name that
45
+ * is already kebab case stays as it is.
46
+ */
47
+ export function blockKey(name: string): string {
48
+ return name.replace(/(?<!^|-)[A-Z]/g, (letter) => `-${letter}`).toLowerCase()
49
+ }
50
+
51
+ /** `star-count` gives `starCount`. */
52
+ export function camelKey(name: string): string {
53
+ return name.replace(/-+([a-z0-9])/gi, (_, letter: string) =>
54
+ letter.toUpperCase()
55
+ )
56
+ }
57
+
58
+ /** The key of `map` that means `key`: the same key, else the same name in kebab or camel case. */
59
+ function sameKey(map: object, key: string): string {
60
+ if (key in map) {
61
+ return key
62
+ }
63
+ const camel = camelKey(key)
64
+ return Object.keys(map).find((other) => camelKey(other) === camel) ?? key
65
+ }
66
+
67
+ function automaticDefinition(name: string): PlutoBlockDefinition {
68
+ return {
69
+ name,
70
+ label: humanizeName(name),
71
+ icon: 'i-lucide-puzzle',
72
+ group: 'site',
73
+ slots: { default: { content: 'blocks' } },
74
+ }
75
+ }
76
+
77
+ function propFromValue(value: unknown): PlutoBlockPropDefinition {
78
+ if (typeof value === 'boolean') {
79
+ return { type: 'boolean', advanced: true }
80
+ }
81
+ if (typeof value === 'number') {
82
+ return { type: 'number', advanced: true }
83
+ }
84
+ const text = typeof value === 'string' ? value : ''
85
+ return {
86
+ type: typeof value === 'string' && !text.includes('\n') ? 'string' : 'text',
87
+ advanced: true,
88
+ }
89
+ }
90
+
91
+ /** The layer of one block in a config. The key can be the kebab name or any form of it. */
92
+ function layerFor(
93
+ config: PlutoBlocksConfig | undefined,
94
+ key: string
95
+ ): PlutoBlockOverride | undefined {
96
+ if (!config) {
97
+ return undefined
98
+ }
99
+ if (key in config) {
100
+ return config[key]
101
+ }
102
+ const other = Object.keys(config).find((name) => blockKey(name) === key)
103
+ return other === undefined ? undefined : config[other]
104
+ }
105
+
106
+ /**
107
+ * Join the entries of one map (props or slots) one by one. An entry of the patch changes the
108
+ * entry of the base key by key, and `false` removes it. An entry with no base starts from `fill`.
109
+ */
110
+ function mergeEntries<T extends object>(
111
+ base: Record<string, T> | undefined,
112
+ patch: Record<string, Partial<T> | false | undefined> | undefined,
113
+ fill: Partial<T>
114
+ ): Record<string, T> {
115
+ const out = { ...base }
116
+ for (const [key, value] of Object.entries(patch ?? {})) {
117
+ if (value === undefined) {
118
+ continue
119
+ }
120
+ const target = sameKey(out, key)
121
+ if (value === false) {
122
+ delete out[target]
123
+ } else {
124
+ out[target] = { ...fill, ...out[target], ...value } as T
125
+ }
126
+ }
127
+ return out
128
+ }
129
+
130
+ /** The same join for two patches: `false` stays in the result, so a layer above can still remove. */
131
+ function mergePatchEntries<T extends object>(
132
+ base: Record<string, Partial<T> | false | undefined> | undefined,
133
+ patch: Record<string, Partial<T> | false | undefined> | undefined
134
+ ): Record<string, Partial<T> | false | undefined> {
135
+ const out = { ...base }
136
+ for (const [key, value] of Object.entries(patch ?? {})) {
137
+ if (value === undefined) {
138
+ continue
139
+ }
140
+ const target = sameKey(out, key)
141
+ const current = out[target]
142
+ out[target] =
143
+ value === false || current === undefined || current === false
144
+ ? value
145
+ : { ...current, ...value }
146
+ }
147
+ return out
148
+ }
149
+
150
+ /**
151
+ * Put a layer over a definition. The fields of the layer replace the fields of the base. Props and
152
+ * slots join key by key (shallow). `false` removes a prop or a slot. `false` for the whole block
153
+ * hides it.
154
+ */
155
+ function mergeDefinition(
156
+ base: PlutoBlockDefinition,
157
+ layer: PlutoBlockOverride | undefined
158
+ ): PlutoBlockDefinition {
159
+ if (layer === false) {
160
+ return { ...base, hidden: true }
161
+ }
162
+ if (typeof layer !== 'object' || layer === null) {
163
+ return base
164
+ }
165
+ return {
166
+ ...base,
167
+ ...layer,
168
+ name: base.name,
169
+ props: mergeEntries(base.props, layer.props, { type: 'string' }),
170
+ slots: mergeEntries(base.slots, layer.slots, {}),
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Put one patch over another, for example the in-file macro over the block made from the
176
+ * component meta. The result is still a patch: `false` for a prop or a slot stays in it.
177
+ */
178
+ export function mergeBlockPatch(
179
+ base: PlutoBlockPatch,
180
+ over: PlutoBlockPatch | false | undefined
181
+ ): PlutoBlockPatch {
182
+ if (over === undefined) {
183
+ return base
184
+ }
185
+ if (over === false) {
186
+ return { ...base, hidden: true }
187
+ }
188
+ const props = mergePatchEntries(base.props, over.props)
189
+ const slots = mergePatchEntries(base.slots, over.slots)
190
+ return {
191
+ ...base,
192
+ ...over,
193
+ ...(Object.keys(props).length ? { props } : {}),
194
+ ...(Object.keys(slots).length ? { slots } : {}),
195
+ } as PlutoBlockPatch
196
+ }
197
+
198
+ /** The slot content kind for a slot that no definition lists. */
199
+ function slotFromChildren(children: PlutoBlock[]): PlutoBlockSlotDefinition {
200
+ const onlyText = children.every((child) => child.kind === 'text')
201
+ return { content: onlyText ? 'rich' : 'blocks' }
202
+ }
203
+
204
+ /**
205
+ * The definition of a component: the preset (or an automatic one), the
206
+ * component layer over it, the site override over that, and the props and
207
+ * slots of `options.block` that none of them lists. Found props go to the
208
+ * Advanced section.
209
+ *
210
+ * The name can be kebab case (`pluto-hero`) or the Pascal form that a page
211
+ * can use (`PlutoHero`). The definition has the kebab name. A prop of the
212
+ * definition takes the spelling that the block uses: when the page has
213
+ * `star-count` and the meta has `starCount`, the key is `star-count`, so an
214
+ * edit changes the prop of the page and adds no second one.
215
+ */
216
+ export function resolveBlockDefinition(
217
+ name: string,
218
+ options: ResolveOptions = {}
219
+ ): PlutoBlockDefinition {
220
+ const key = blockKey(name)
221
+ const { components, overrides } = options.sources ?? {}
222
+ const merged = mergeDefinition(
223
+ mergeDefinition(
224
+ BLOCK_PRESETS[key] ?? automaticDefinition(key),
225
+ layerFor(components, key)
226
+ ),
227
+ layerFor(overrides, key)
228
+ )
229
+ const found = options.block?.kind === 'component' ? options.block : undefined
230
+ if (!found) {
231
+ return merged
232
+ }
233
+ const props: Record<string, PlutoBlockPropDefinition> = {}
234
+ for (const [prop, definition] of Object.entries(merged.props ?? {})) {
235
+ props[sameKey(found.props, prop)] = definition
236
+ }
237
+ for (const [prop, value] of Object.entries(found.props)) {
238
+ if (
239
+ !(prop in props) &&
240
+ !(HIDDEN_PROP_NAMES as readonly string[]).includes(prop)
241
+ ) {
242
+ props[prop] = propFromValue(value)
243
+ }
244
+ }
245
+ const slots = { ...merged.slots }
246
+ for (const [slot, children] of Object.entries(found.slots)) {
247
+ if (!(slot in slots)) {
248
+ slots[slot] = slotFromChildren(children)
249
+ }
250
+ }
251
+ return { ...merged, props, slots }
252
+ }
253
+
254
+ /** The text a palette inserts for a component. */
255
+ export function insertTextFor(definition: PlutoBlockDefinition): string {
256
+ return definition.insert ?? `::${definition.name}\n::`
257
+ }
258
+
259
+ export interface PaletteGroup {
260
+ group: string
261
+ items: PlutoBlockDefinition[]
262
+ }
263
+
264
+ /**
265
+ * The blocks of the palette, in groups. The presets come first, then the
266
+ * components of the component layer (a component shows only when it is in
267
+ * that layer), then the components that only the site config declares. A
268
+ * hidden block, or a block set to `false`, is not in the list.
269
+ */
270
+ export function listPaletteBlocks(
271
+ sources: PlutoBlockSources = {}
272
+ ): PaletteGroup[] {
273
+ const names = new Set([
274
+ ...Object.keys(BLOCK_PRESETS),
275
+ ...Object.keys(sources.components ?? {}).map(blockKey),
276
+ ...Object.keys(sources.overrides ?? {}).map(blockKey),
277
+ ])
278
+ const groups = new Map<string, PlutoBlockDefinition[]>()
279
+ for (const name of names) {
280
+ const definition = resolveBlockDefinition(name, { sources })
281
+ if (definition.hidden) {
282
+ continue
283
+ }
284
+ groups.set(definition.group, [
285
+ ...(groups.get(definition.group) ?? []),
286
+ definition,
287
+ ])
288
+ }
289
+ const known = BLOCK_GROUPS as readonly string[]
290
+ const order = [
291
+ ...known.filter((group) => groups.has(group)),
292
+ ...[...groups.keys()].filter((group) => !known.includes(group)),
293
+ ]
294
+ return order.map((group) => ({ group, items: groups.get(group)! }))
295
+ }
@@ -0,0 +1,107 @@
1
+ // Write a block doc back to text. A block with `original` writes its source
2
+ // bytes. The gaps between blocks are always the source text. Only an edited
3
+ // block is built from the model, so one edit changes only the lines of that block.
4
+
5
+ import type {
6
+ PlutoBlock,
7
+ PlutoBlockDoc,
8
+ PlutoCodeBlock,
9
+ PlutoComponentBlock,
10
+ } from './types'
11
+ import {
12
+ completeOrder,
13
+ isSameProps,
14
+ renderInlineProps,
15
+ renderYamlProps,
16
+ } from './mdc-props'
17
+ import { flatChildren } from './tree'
18
+
19
+ /** Puts the indent before each non-blank line but the first. */
20
+ export function indentLines(text: string, indent: string): string {
21
+ if (!indent) {
22
+ return text
23
+ }
24
+ return text
25
+ .split('\n')
26
+ .map((line, index) => (index > 0 && line !== '' ? indent + line : line))
27
+ .join('\n')
28
+ }
29
+
30
+ function renderCode(block: PlutoCodeBlock): string {
31
+ const char = block.fence[0] ?? '`'
32
+ let fence = block.fence.length >= 3 ? block.fence : char.repeat(3)
33
+ // A line of the code that looks like the fence needs a longer fence.
34
+ for (const line of block.code.split('\n')) {
35
+ const match = /^\s*([`~]{3,})\s*$/.exec(line)
36
+ if (match && match[1]![0] === char && match[1]!.length >= fence.length) {
37
+ fence = char.repeat(match[1]!.length + 1)
38
+ }
39
+ }
40
+ const info = [
41
+ block.lang,
42
+ block.filename === undefined ? '' : `[${block.filename}]`,
43
+ block.meta ?? '',
44
+ ]
45
+ .filter(Boolean)
46
+ .join(' ')
47
+ const body = block.code === '' ? '' : `\n${block.code}`
48
+ return indentLines(`${fence}${info}${body}\n${fence}`, block.indent ?? '')
49
+ }
50
+
51
+ /** The opening line of a component, with its props. Unchanged props give the source text. */
52
+ export function renderOpener(block: PlutoComponentBlock): string {
53
+ if (isSameProps(block.props, block.propOrder, block.propSource)) {
54
+ return block.openerText
55
+ }
56
+ const order = completeOrder(block.props, block.propOrder)
57
+ const head = `${':'.repeat(block.colons)}${block.name}`
58
+ if (block.propsStyle === 'yaml' && block.yamlHead !== undefined) {
59
+ const entries = renderYamlProps(block.props, order, block.propSource)
60
+ if (entries.length === 0) {
61
+ return head
62
+ }
63
+ const fenceIndent = block.yamlHead.slice(
64
+ block.yamlHead.lastIndexOf('\n') + 1
65
+ )
66
+ const yamlIndent = block.yamlIndent ?? ''
67
+ const inner = entries
68
+ .map((entry) => yamlIndent + indentLines(entry, yamlIndent))
69
+ .join('\n')
70
+ return `${head}${block.yamlHead}---\n${inner}\n${fenceIndent}---`
71
+ }
72
+ const body = renderInlineProps(block.props, order, block.propSource)
73
+ return body ? `${head}{${body}}` : head
74
+ }
75
+
76
+ function renderComponent(block: PlutoComponentBlock): string {
77
+ const children = flatChildren(block)
78
+ let out = renderOpener(block)
79
+ for (const [index, child] of children.entries()) {
80
+ out += (block.gaps[index] ?? '') + renderBlock(child)
81
+ }
82
+ return `${out}${block.gaps[children.length] ?? ''}${':'.repeat(block.colons)}`
83
+ }
84
+
85
+ /** The text of one block, without the white space before its first line. */
86
+ export function renderBlock(block: PlutoBlock): string {
87
+ if (block.original) {
88
+ return block.original.text
89
+ }
90
+ switch (block.kind) {
91
+ case 'text':
92
+ case 'raw':
93
+ return indentLines(block.markdown, block.indent ?? '')
94
+ case 'code':
95
+ return renderCode(block)
96
+ case 'component':
97
+ return renderComponent(block)
98
+ }
99
+ }
100
+
101
+ export function serializeBlocks(doc: PlutoBlockDoc): string {
102
+ let out = doc.gaps[0] ?? ''
103
+ for (const [index, block] of doc.blocks.entries()) {
104
+ out += renderBlock(block) + (doc.gaps[index + 1] ?? '')
105
+ }
106
+ return out
107
+ }
@@ -0,0 +1,85 @@
1
+ // Helpers to walk the block tree. No Nuxt import.
2
+
3
+ import type {
4
+ PlutoBlock,
5
+ PlutoBlockDoc,
6
+ PlutoBlockLocation,
7
+ PlutoComponentBlock,
8
+ } from './types'
9
+
10
+ /** The children of a component in source order, over all slots. */
11
+ export function flatChildren(component: PlutoComponentBlock): PlutoBlock[] {
12
+ return Object.values(component.slots).flat()
13
+ }
14
+
15
+ /** Calls `visit` for each block, parents first. */
16
+ export function walkBlocks(
17
+ blocks: PlutoBlock[],
18
+ visit: (block: PlutoBlock, parent?: PlutoComponentBlock) => void,
19
+ parent?: PlutoComponentBlock
20
+ ): void {
21
+ for (const block of blocks) {
22
+ visit(block, parent)
23
+ if (block.kind === 'component') {
24
+ walkBlocks(flatChildren(block), visit, block)
25
+ }
26
+ }
27
+ }
28
+
29
+ export function findBlock(
30
+ doc: PlutoBlockDoc,
31
+ id: string
32
+ ): PlutoBlockLocation | undefined {
33
+ const search = (
34
+ blocks: PlutoBlock[],
35
+ parent?: PlutoComponentBlock,
36
+ slot?: string
37
+ ): PlutoBlockLocation | undefined => {
38
+ for (const [index, block] of blocks.entries()) {
39
+ if (block.id === id) {
40
+ return { block, parent, slot, index }
41
+ }
42
+ if (block.kind === 'component') {
43
+ for (const [name, children] of Object.entries(block.slots)) {
44
+ const found = search(children, block, name)
45
+ if (found) {
46
+ return found
47
+ }
48
+ }
49
+ }
50
+ }
51
+ return undefined
52
+ }
53
+ return search(doc.blocks)
54
+ }
55
+
56
+ /** The ids from the top-level block down to the block. Empty when the id is unknown. */
57
+ export function pathOf(doc: PlutoBlockDoc, id: string): string[] {
58
+ const search = (
59
+ blocks: PlutoBlock[],
60
+ trail: string[]
61
+ ): string[] | undefined => {
62
+ for (const block of blocks) {
63
+ const next = [...trail, block.id]
64
+ if (block.id === id) {
65
+ return next
66
+ }
67
+ if (block.kind === 'component') {
68
+ const found = search(flatChildren(block), next)
69
+ if (found) {
70
+ return found
71
+ }
72
+ }
73
+ }
74
+ return undefined
75
+ }
76
+ return search(doc.blocks, []) ?? []
77
+ }
78
+
79
+ let counter = 0
80
+
81
+ /** A new id for a block that is not from a parse. */
82
+ export function newBlockId(): string {
83
+ counter++
84
+ return `n${counter}`
85
+ }
@@ -0,0 +1,195 @@
1
+ // Types of the block model. The model has no Nuxt import, so a package can
2
+ // own it later. The truth stays the Markdown string: a block with `original`
3
+ // is written back byte for byte.
4
+
5
+ /** A part of the source text that a block came from. */
6
+ export interface PlutoSlice {
7
+ start: number
8
+ end: number
9
+ text: string
10
+ }
11
+
12
+ interface PlutoBlockBase {
13
+ id: string
14
+ /** The source text of the block. An edit removes it from the block and its ancestors. */
15
+ original?: PlutoSlice
16
+ /** The white space before the first line of the block, inside a component. */
17
+ indent?: string
18
+ }
19
+
20
+ export interface PlutoTextBlock extends PlutoBlockBase {
21
+ kind: 'text'
22
+ /** Without the indent of the parent component. */
23
+ markdown: string
24
+ }
25
+
26
+ export interface PlutoCodeBlock extends PlutoBlockBase {
27
+ kind: 'code'
28
+ lang: string
29
+ filename?: string
30
+ /** The rest of the info string after the language and the file name. */
31
+ meta?: string
32
+ /** The fence characters, for example three backticks. */
33
+ fence: string
34
+ /** Without the indent of the parent component. */
35
+ code: string
36
+ }
37
+
38
+ export type PlutoPropValue = unknown
39
+
40
+ export type PlutoPropsStyle = 'inline' | 'yaml'
41
+
42
+ export interface PlutoComponentBlock extends PlutoBlockBase {
43
+ kind: 'component'
44
+ name: string
45
+ props: Record<string, PlutoPropValue>
46
+ /** Where the props are written: after the name `{a="b"}` or in a YAML block. */
47
+ propsStyle: PlutoPropsStyle
48
+ /** The order of the props in the source. New props go at the end. */
49
+ propOrder: string[]
50
+ /**
51
+ * The source of each prop, for a prop that has not changed. The key is a
52
+ * prop name. The value is the text of the token, or of the YAML entry.
53
+ */
54
+ propSource: Record<string, { value: PlutoPropValue; text: string }>
55
+ /** The colons of the opening line. */
56
+ colons: number
57
+ /** The slots in the order of the source. `default` is the content before the first `#name`. */
58
+ slots: Record<string, PlutoBlock[]>
59
+ /**
60
+ * The text between the children, in source order, over all slots. It has the
61
+ * `#name` lines of the slots. Its length is the number of children plus one.
62
+ */
63
+ gaps: string[]
64
+ /** For `yaml` style: the text between the opening line and the YAML block. */
65
+ yamlHead?: string
66
+ /** The opening line as it was in the source, for the check "did the props change". */
67
+ openerText: string
68
+ /** For `yaml` style: the indent of the YAML entries. */
69
+ yamlIndent?: string
70
+ }
71
+
72
+ export type PlutoRawReason =
73
+ 'table' | 'html' | 'inline-mdc' | 'footnote' | 'task-list' | 'unknown'
74
+
75
+ export interface PlutoRawBlock extends PlutoBlockBase {
76
+ kind: 'raw'
77
+ markdown: string
78
+ reason: PlutoRawReason
79
+ }
80
+
81
+ export type PlutoBlock =
82
+ PlutoTextBlock | PlutoCodeBlock | PlutoComponentBlock | PlutoRawBlock
83
+
84
+ export interface PlutoBlockDoc {
85
+ blocks: PlutoBlock[]
86
+ /** The original text before, between and after the blocks. Its length is the number of blocks plus one. */
87
+ gaps: string[]
88
+ }
89
+
90
+ /** Where a block is in the tree. `parent` is undefined for a top-level block. */
91
+ export interface PlutoBlockLocation {
92
+ block: PlutoBlock
93
+ parent?: PlutoComponentBlock
94
+ slot?: string
95
+ /** The index in the list of the document, or in the slot. */
96
+ index: number
97
+ }
98
+
99
+ // Block definitions: what the editor shows for a component.
100
+
101
+ export type PlutoBlockPropType =
102
+ | 'string'
103
+ | 'text'
104
+ | 'boolean'
105
+ | 'number'
106
+ | 'select'
107
+ | 'icon'
108
+ | 'color'
109
+ | 'link'
110
+
111
+ export interface PlutoBlockPropOption {
112
+ value: string
113
+ /** A plain text or an i18n key. */
114
+ label?: string
115
+ }
116
+
117
+ export interface PlutoBlockPropDefinition {
118
+ type: PlutoBlockPropType
119
+ /** A plain text or an i18n key. */
120
+ label?: string
121
+ description?: string
122
+ options?: Array<string | PlutoBlockPropOption>
123
+ /** A prop in the Advanced section of the panel. */
124
+ advanced?: boolean
125
+ /**
126
+ * A slot with the same text, for example the `default` slot of a button and its `label`. The
127
+ * panel shows one field for both: it edits the slot when the block has text in it (the slot wins
128
+ * on the site), else the prop. The slot does not show in the Content section.
129
+ */
130
+ slot?: string
131
+ /**
132
+ * The value the component uses when the prop is missing. The control shows it as the
133
+ * placeholder (or in the "Default" item of a choice). It never goes into the Markdown.
134
+ */
135
+ default?: string | number | boolean
136
+ }
137
+
138
+ /** The editor for the content of a slot. */
139
+ export type PlutoBlockSlotContent = 'rich' | 'inline' | 'blocks'
140
+
141
+ export interface PlutoBlockSlotDefinition {
142
+ label?: string
143
+ content?: PlutoBlockSlotContent
144
+ /** For `blocks`: the component names that the slot accepts. */
145
+ accepts?: string[]
146
+ }
147
+
148
+ export interface PlutoBlockDefinition {
149
+ name: string
150
+ /** A plain text or an i18n key. */
151
+ label: string
152
+ /** A plain text or an i18n key. */
153
+ description?: string
154
+ icon: string
155
+ group: string
156
+ props?: Record<string, PlutoBlockPropDefinition>
157
+ slots?: Record<string, PlutoBlockSlotDefinition>
158
+ /** The Markdown that the palette inserts. */
159
+ insert?: string
160
+ /** `false` shows a placeholder card instead of the component in the preview. */
161
+ preview?: boolean
162
+ /** Not in the palette. The block still shows with its label. */
163
+ hidden?: boolean
164
+ }
165
+
166
+ /** A change to one prop: a partial prop definition. A prop with no `type` is a `string`. */
167
+ export type PlutoBlockPropPatch = Partial<PlutoBlockPropDefinition>
168
+
169
+ /**
170
+ * A partial block definition: one layer of the sources (the component layer, or the site
171
+ * override). `Props` is the prop interface of a component, so `definePlutoBlock<Props>()` can
172
+ * check the keys of `props`. A prop or a slot set to `false` is removed from the layers below.
173
+ */
174
+ export interface PlutoBlockPatch<
175
+ Props extends object = Record<string, unknown>,
176
+ > extends Partial<Omit<PlutoBlockDefinition, 'name' | 'props' | 'slots'>> {
177
+ props?: { [Key in keyof Props & string]?: PlutoBlockPropPatch | false }
178
+ slots?: Record<string, PlutoBlockSlotDefinition | false>
179
+ }
180
+
181
+ /** One layer for one block: a partial definition, or `false` to hide the block from the palette. */
182
+ export type PlutoBlockOverride = PlutoBlockPatch | false
183
+
184
+ /** The blocks of one layer, by component name (kebab case). */
185
+ export type PlutoBlocksConfig = Record<string, PlutoBlockOverride>
186
+
187
+ /**
188
+ * The layers that join the preset into a block definition. The component layer comes from the
189
+ * component files (it is empty until the build step fills it). The overrides are `plutoBlocks`
190
+ * in `app.config.ts`, and they win.
191
+ */
192
+ export interface PlutoBlockSources {
193
+ components?: PlutoBlocksConfig
194
+ overrides?: PlutoBlocksConfig
195
+ }