@plutocms/docs 0.15.0 → 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.
- package/CHANGELOG.md +8 -0
- package/FEATURES.md +4 -0
- package/README.md +1 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +4 -2
- package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +4 -2
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPalette.vue +16 -15
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +1 -2
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +32 -9
- package/app/composables/pluto-block-editor.ts +23 -7
- package/modules/pluto-docs/blocks.ts +217 -0
- package/modules/pluto-docs/index.ts +108 -1
- package/modules/pluto-docs/options.ts +35 -1
- package/package.json +3 -2
- package/shared/types/pluto-blocks.d.ts +10 -0
- package/shared/utils/blocks/component-meta.ts +285 -0
- package/shared/utils/blocks/define.ts +15 -0
- package/shared/utils/blocks/editor.ts +11 -11
- package/shared/utils/blocks/presets.ts +6 -5
- package/shared/utils/blocks/registry.ts +175 -43
- package/shared/utils/blocks/types.ts +33 -3
|
@@ -1,38 +1,69 @@
|
|
|
1
|
-
// Join the presets, the site overrides (`plutoBlocks` in
|
|
2
|
-
// props found in the document into one definition for
|
|
3
|
-
// functions: the caller reads the app config and passes
|
|
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.
|
|
4
5
|
|
|
5
6
|
import type {
|
|
6
7
|
PlutoBlock,
|
|
7
8
|
PlutoBlockDefinition,
|
|
8
9
|
PlutoBlockOverride,
|
|
10
|
+
PlutoBlockPatch,
|
|
9
11
|
PlutoBlockPropDefinition,
|
|
10
12
|
PlutoBlocksConfig,
|
|
11
13
|
PlutoBlockSlotDefinition,
|
|
14
|
+
PlutoBlockSources,
|
|
12
15
|
PlutoComponentBlock,
|
|
13
16
|
} from './types'
|
|
14
17
|
import { BLOCK_GROUPS, BLOCK_PRESETS, HIDDEN_PROP_NAMES } from './presets'
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
T extends Partial<Omit<PlutoBlockDefinition, 'name'>>,
|
|
19
|
-
>(definition: T): T {
|
|
20
|
-
return definition
|
|
21
|
-
}
|
|
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'
|
|
22
21
|
|
|
23
22
|
export interface ResolveOptions {
|
|
24
|
-
/** The `plutoBlocks` value of the app config. */
|
|
25
|
-
|
|
23
|
+
/** The layers: the component layer and the `plutoBlocks` value of the app config. */
|
|
24
|
+
sources?: PlutoBlockSources
|
|
26
25
|
/** A block of the doc. Its props and slots fill what the definition does not list. */
|
|
27
26
|
block?: PlutoComponentBlock
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
/** A readable name from a component name: `pluto-hero` gives `Pluto hero`. */
|
|
29
|
+
/** A readable name from a component name: `pluto-hero` gives `Pluto hero`, `starCount` gives `Star count`. */
|
|
31
30
|
export function humanizeName(name: string): string {
|
|
32
|
-
const text = name
|
|
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()
|
|
33
39
|
return text ? text[0]!.toUpperCase() + text.slice(1) : name
|
|
34
40
|
}
|
|
35
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
|
+
|
|
36
67
|
function automaticDefinition(name: string): PlutoBlockDefinition {
|
|
37
68
|
return {
|
|
38
69
|
name,
|
|
@@ -57,31 +88,113 @@ function propFromValue(value: unknown): PlutoBlockPropDefinition {
|
|
|
57
88
|
}
|
|
58
89
|
}
|
|
59
90
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
64
128
|
}
|
|
65
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
|
+
*/
|
|
66
155
|
function mergeDefinition(
|
|
67
156
|
base: PlutoBlockDefinition,
|
|
68
|
-
|
|
157
|
+
layer: PlutoBlockOverride | undefined
|
|
69
158
|
): PlutoBlockDefinition {
|
|
70
|
-
if (
|
|
159
|
+
if (layer === false) {
|
|
71
160
|
return { ...base, hidden: true }
|
|
72
161
|
}
|
|
73
|
-
if (
|
|
162
|
+
if (typeof layer !== 'object' || layer === null) {
|
|
74
163
|
return base
|
|
75
164
|
}
|
|
76
165
|
return {
|
|
77
166
|
...base,
|
|
78
|
-
...
|
|
167
|
+
...layer,
|
|
79
168
|
name: base.name,
|
|
80
|
-
props:
|
|
81
|
-
slots:
|
|
169
|
+
props: mergeEntries(base.props, layer.props, { type: 'string' }),
|
|
170
|
+
slots: mergeEntries(base.slots, layer.slots, {}),
|
|
82
171
|
}
|
|
83
172
|
}
|
|
84
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
|
+
|
|
85
198
|
/** The slot content kind for a slot that no definition lists. */
|
|
86
199
|
function slotFromChildren(children: PlutoBlock[]): PlutoBlockSlotDefinition {
|
|
87
200
|
const onlyText = children.every((child) => child.kind === 'text')
|
|
@@ -89,33 +202,50 @@ function slotFromChildren(children: PlutoBlock[]): PlutoBlockSlotDefinition {
|
|
|
89
202
|
}
|
|
90
203
|
|
|
91
204
|
/**
|
|
92
|
-
* The definition of a component: the preset (or an automatic one), the
|
|
93
|
-
*
|
|
94
|
-
* lists. Found props go to the
|
|
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.
|
|
95
215
|
*/
|
|
96
216
|
export function resolveBlockDefinition(
|
|
97
217
|
name: string,
|
|
98
218
|
options: ResolveOptions = {}
|
|
99
219
|
): PlutoBlockDefinition {
|
|
100
|
-
const
|
|
101
|
-
const
|
|
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
|
+
)
|
|
102
229
|
const found = options.block?.kind === 'component' ? options.block : undefined
|
|
103
230
|
if (!found) {
|
|
104
231
|
return merged
|
|
105
232
|
}
|
|
106
|
-
const props = {
|
|
107
|
-
for (const [
|
|
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)) {
|
|
108
238
|
if (
|
|
109
|
-
!(
|
|
110
|
-
!(HIDDEN_PROP_NAMES as readonly string[]).includes(
|
|
239
|
+
!(prop in props) &&
|
|
240
|
+
!(HIDDEN_PROP_NAMES as readonly string[]).includes(prop)
|
|
111
241
|
) {
|
|
112
|
-
props[
|
|
242
|
+
props[prop] = propFromValue(value)
|
|
113
243
|
}
|
|
114
244
|
}
|
|
115
245
|
const slots = { ...merged.slots }
|
|
116
|
-
for (const [
|
|
117
|
-
if (!(
|
|
118
|
-
slots[
|
|
246
|
+
for (const [slot, children] of Object.entries(found.slots)) {
|
|
247
|
+
if (!(slot in slots)) {
|
|
248
|
+
slots[slot] = slotFromChildren(children)
|
|
119
249
|
}
|
|
120
250
|
}
|
|
121
251
|
return { ...merged, props, slots }
|
|
@@ -133,19 +263,21 @@ export interface PaletteGroup {
|
|
|
133
263
|
|
|
134
264
|
/**
|
|
135
265
|
* The blocks of the palette, in groups. The presets come first, then the
|
|
136
|
-
* components
|
|
137
|
-
*
|
|
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.
|
|
138
269
|
*/
|
|
139
270
|
export function listPaletteBlocks(
|
|
140
|
-
|
|
271
|
+
sources: PlutoBlockSources = {}
|
|
141
272
|
): PaletteGroup[] {
|
|
142
|
-
const names = [
|
|
273
|
+
const names = new Set([
|
|
143
274
|
...Object.keys(BLOCK_PRESETS),
|
|
144
|
-
...Object.keys(
|
|
145
|
-
|
|
275
|
+
...Object.keys(sources.components ?? {}).map(blockKey),
|
|
276
|
+
...Object.keys(sources.overrides ?? {}).map(blockKey),
|
|
277
|
+
])
|
|
146
278
|
const groups = new Map<string, PlutoBlockDefinition[]>()
|
|
147
279
|
for (const name of names) {
|
|
148
|
-
const definition = resolveBlockDefinition(name, {
|
|
280
|
+
const definition = resolveBlockDefinition(name, { sources })
|
|
149
281
|
if (definition.hidden) {
|
|
150
282
|
continue
|
|
151
283
|
}
|
|
@@ -128,6 +128,11 @@ export interface PlutoBlockPropDefinition {
|
|
|
128
128
|
* on the site), else the prop. The slot does not show in the Content section.
|
|
129
129
|
*/
|
|
130
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
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
/** The editor for the content of a slot. */
|
|
@@ -158,8 +163,33 @@ export interface PlutoBlockDefinition {
|
|
|
158
163
|
hidden?: boolean
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
/** A
|
|
162
|
-
export type
|
|
163
|
-
|
|
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
|
|
164
183
|
|
|
184
|
+
/** The blocks of one layer, by component name (kebab case). */
|
|
165
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
|
+
}
|