@plutocms/docs 0.0.0-stage → 0.15.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/.gitattributes +1 -0
- package/.prettierignore +5 -0
- package/CHANGELOG.md +178 -0
- package/FEATURES.md +30 -0
- package/README.md +39 -2
- package/app/assets/css/pluto-layer.css +40 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +163 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockInspector.vue +519 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +159 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockModeSwitch.vue +46 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockOutline.vue +146 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockOutlineList.vue +347 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPalette.vue +85 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +233 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +246 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockRawEditor.vue +62 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockTextEditor.vue +92 -0
- package/app/components/Pluto/Docs/Blocks/PlutoPreviewBlock.vue +80 -0
- package/app/components/Pluto/Docs/PlutoFieldMdc.vue +117 -0
- package/app/components/Pluto/Docs/PlutoMdcInsertMenu.vue +36 -0
- package/app/components/PlutoContentPreview.vue +35 -0
- package/app/composables/pluto-block-editor.ts +443 -0
- package/app/composables/pluto-content-edit-link.ts +19 -0
- package/app/composables/pluto-content-preview.ts +29 -0
- package/app/composables/pluto-content-runtime.ts +237 -0
- package/app/pages/_pluto/preview.vue +555 -0
- package/app/plugins/pluto-docs.ts +50 -0
- package/i18n/locales/en.json +253 -0
- package/i18n/locales/pt-BR.json +289 -0
- package/modules/pluto-docs/index.ts +123 -0
- package/modules/pluto-docs/manifest.ts +159 -0
- package/modules/pluto-docs/options.ts +224 -0
- package/nuxt.config.ts +36 -0
- package/package.json +55 -4
- package/server/handlers/navigation.get.ts +7 -0
- package/server/handlers/page.get.ts +7 -0
- package/server/handlers/surround.get.ts +7 -0
- package/server/plugins/pluto-docs.ts +25 -0
- package/server/utils/pluto-docs-runtime.ts +504 -0
- package/shared/types/docs.d.ts +40 -0
- package/shared/types/pluto-manifest.d.ts +6 -0
- package/shared/utils/blocks/editor.ts +357 -0
- package/shared/utils/blocks/history.ts +94 -0
- package/shared/utils/blocks/mdc-props.ts +321 -0
- package/shared/utils/blocks/operations.ts +692 -0
- package/shared/utils/blocks/parse.ts +444 -0
- package/shared/utils/blocks/presets.ts +383 -0
- package/shared/utils/blocks/preview-marks.ts +98 -0
- package/shared/utils/blocks/preview-protocol.ts +195 -0
- package/shared/utils/blocks/registry.ts +163 -0
- package/shared/utils/blocks/serialize.ts +107 -0
- package/shared/utils/blocks/tree.ts +85 -0
- package/shared/utils/blocks/types.ts +165 -0
- package/shared/utils/content-route.ts +40 -0
- package/shared/utils/docs-type.ts +290 -0
- package/shared/utils/mdc-snippets.ts +114 -0
- package/shared/utils/navigation-config.ts +19 -0
- package/shared/utils/runtime-content.ts +403 -0
- package/tailwind.lint.css +10 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Join the presets, the site overrides (`plutoBlocks` in app.config.ts) and the
|
|
2
|
+
// props found in the document into one definition for each component. Pure
|
|
3
|
+
// functions: the caller reads the app config and passes it in.
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
PlutoBlock,
|
|
7
|
+
PlutoBlockDefinition,
|
|
8
|
+
PlutoBlockOverride,
|
|
9
|
+
PlutoBlockPropDefinition,
|
|
10
|
+
PlutoBlocksConfig,
|
|
11
|
+
PlutoBlockSlotDefinition,
|
|
12
|
+
PlutoComponentBlock,
|
|
13
|
+
} from './types'
|
|
14
|
+
import { BLOCK_GROUPS, BLOCK_PRESETS, HIDDEN_PROP_NAMES } from './presets'
|
|
15
|
+
|
|
16
|
+
/** The definition of a block that a site declares in `app.config.ts`. It gives type help. */
|
|
17
|
+
export function definePlutoBlock<
|
|
18
|
+
T extends Partial<Omit<PlutoBlockDefinition, 'name'>>,
|
|
19
|
+
>(definition: T): T {
|
|
20
|
+
return definition
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ResolveOptions {
|
|
24
|
+
/** The `plutoBlocks` value of the app config. */
|
|
25
|
+
overrides?: PlutoBlocksConfig
|
|
26
|
+
/** A block of the doc. Its props and slots fill what the definition does not list. */
|
|
27
|
+
block?: PlutoComponentBlock
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A readable name from a component name: `pluto-hero` gives `Pluto hero`. */
|
|
31
|
+
export function humanizeName(name: string): string {
|
|
32
|
+
const text = name.replace(/^u-/, '').replace(/[-_]+/g, ' ').trim()
|
|
33
|
+
return text ? text[0]!.toUpperCase() + text.slice(1) : name
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function automaticDefinition(name: string): PlutoBlockDefinition {
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
label: humanizeName(name),
|
|
40
|
+
icon: 'i-lucide-puzzle',
|
|
41
|
+
group: 'site',
|
|
42
|
+
slots: { default: { content: 'blocks' } },
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function propFromValue(value: unknown): PlutoBlockPropDefinition {
|
|
47
|
+
if (typeof value === 'boolean') {
|
|
48
|
+
return { type: 'boolean', advanced: true }
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'number') {
|
|
51
|
+
return { type: 'number', advanced: true }
|
|
52
|
+
}
|
|
53
|
+
const text = typeof value === 'string' ? value : ''
|
|
54
|
+
return {
|
|
55
|
+
type: typeof value === 'string' && !text.includes('\n') ? 'string' : 'text',
|
|
56
|
+
advanced: true,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isOverrideObject(
|
|
61
|
+
override: PlutoBlockOverride | undefined
|
|
62
|
+
): override is Exclude<PlutoBlockOverride, false> {
|
|
63
|
+
return typeof override === 'object' && override !== null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mergeDefinition(
|
|
67
|
+
base: PlutoBlockDefinition,
|
|
68
|
+
override: PlutoBlockOverride | undefined
|
|
69
|
+
): PlutoBlockDefinition {
|
|
70
|
+
if (override === false) {
|
|
71
|
+
return { ...base, hidden: true }
|
|
72
|
+
}
|
|
73
|
+
if (!isOverrideObject(override)) {
|
|
74
|
+
return base
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
...base,
|
|
78
|
+
...override,
|
|
79
|
+
name: base.name,
|
|
80
|
+
props: { ...base.props, ...override.props },
|
|
81
|
+
slots: { ...base.slots, ...override.slots },
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The slot content kind for a slot that no definition lists. */
|
|
86
|
+
function slotFromChildren(children: PlutoBlock[]): PlutoBlockSlotDefinition {
|
|
87
|
+
const onlyText = children.every((child) => child.kind === 'text')
|
|
88
|
+
return { content: onlyText ? 'rich' : 'blocks' }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The definition of a component: the preset (or an automatic one), the site
|
|
93
|
+
* override over it, and the props and slots of `options.block` that neither
|
|
94
|
+
* lists. Found props go to the Advanced section.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveBlockDefinition(
|
|
97
|
+
name: string,
|
|
98
|
+
options: ResolveOptions = {}
|
|
99
|
+
): PlutoBlockDefinition {
|
|
100
|
+
const base = BLOCK_PRESETS[name] ?? automaticDefinition(name)
|
|
101
|
+
const merged = mergeDefinition(base, options.overrides?.[name])
|
|
102
|
+
const found = options.block?.kind === 'component' ? options.block : undefined
|
|
103
|
+
if (!found) {
|
|
104
|
+
return merged
|
|
105
|
+
}
|
|
106
|
+
const props = { ...merged.props }
|
|
107
|
+
for (const [key, value] of Object.entries(found.props)) {
|
|
108
|
+
if (
|
|
109
|
+
!(key in props) &&
|
|
110
|
+
!(HIDDEN_PROP_NAMES as readonly string[]).includes(key)
|
|
111
|
+
) {
|
|
112
|
+
props[key] = propFromValue(value)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const slots = { ...merged.slots }
|
|
116
|
+
for (const [key, children] of Object.entries(found.slots)) {
|
|
117
|
+
if (!(key in slots)) {
|
|
118
|
+
slots[key] = slotFromChildren(children)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { ...merged, props, slots }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The text a palette inserts for a component. */
|
|
125
|
+
export function insertTextFor(definition: PlutoBlockDefinition): string {
|
|
126
|
+
return definition.insert ?? `::${definition.name}\n::`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface PaletteGroup {
|
|
130
|
+
group: string
|
|
131
|
+
items: PlutoBlockDefinition[]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The blocks of the palette, in groups. The presets come first, then the
|
|
136
|
+
* components that only the site config declares. A hidden block, or a block set
|
|
137
|
+
* to `false`, is not in the list.
|
|
138
|
+
*/
|
|
139
|
+
export function listPaletteBlocks(
|
|
140
|
+
overrides: PlutoBlocksConfig = {}
|
|
141
|
+
): PaletteGroup[] {
|
|
142
|
+
const names = [
|
|
143
|
+
...Object.keys(BLOCK_PRESETS),
|
|
144
|
+
...Object.keys(overrides).filter((name) => !(name in BLOCK_PRESETS)),
|
|
145
|
+
]
|
|
146
|
+
const groups = new Map<string, PlutoBlockDefinition[]>()
|
|
147
|
+
for (const name of names) {
|
|
148
|
+
const definition = resolveBlockDefinition(name, { overrides })
|
|
149
|
+
if (definition.hidden) {
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
groups.set(definition.group, [
|
|
153
|
+
...(groups.get(definition.group) ?? []),
|
|
154
|
+
definition,
|
|
155
|
+
])
|
|
156
|
+
}
|
|
157
|
+
const known = BLOCK_GROUPS as readonly string[]
|
|
158
|
+
const order = [
|
|
159
|
+
...known.filter((group) => groups.has(group)),
|
|
160
|
+
...[...groups.keys()].filter((group) => !known.includes(group)),
|
|
161
|
+
]
|
|
162
|
+
return order.map((group) => ({ group, items: groups.get(group)! }))
|
|
163
|
+
}
|
|
@@ -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,165 @@
|
|
|
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
|
+
|
|
133
|
+
/** The editor for the content of a slot. */
|
|
134
|
+
export type PlutoBlockSlotContent = 'rich' | 'inline' | 'blocks'
|
|
135
|
+
|
|
136
|
+
export interface PlutoBlockSlotDefinition {
|
|
137
|
+
label?: string
|
|
138
|
+
content?: PlutoBlockSlotContent
|
|
139
|
+
/** For `blocks`: the component names that the slot accepts. */
|
|
140
|
+
accepts?: string[]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface PlutoBlockDefinition {
|
|
144
|
+
name: string
|
|
145
|
+
/** A plain text or an i18n key. */
|
|
146
|
+
label: string
|
|
147
|
+
/** A plain text or an i18n key. */
|
|
148
|
+
description?: string
|
|
149
|
+
icon: string
|
|
150
|
+
group: string
|
|
151
|
+
props?: Record<string, PlutoBlockPropDefinition>
|
|
152
|
+
slots?: Record<string, PlutoBlockSlotDefinition>
|
|
153
|
+
/** The Markdown that the palette inserts. */
|
|
154
|
+
insert?: string
|
|
155
|
+
/** `false` shows a placeholder card instead of the component in the preview. */
|
|
156
|
+
preview?: boolean
|
|
157
|
+
/** Not in the palette. The block still shows with its label. */
|
|
158
|
+
hidden?: boolean
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A site override: a partial definition, or `false` to hide the block from the palette. */
|
|
162
|
+
export type PlutoBlockOverride =
|
|
163
|
+
Partial<Omit<PlutoBlockDefinition, 'name'>> | false
|
|
164
|
+
|
|
165
|
+
export type PlutoBlocksConfig = Record<string, PlutoBlockOverride>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const NUMBER_PREFIX = /^\d+\./
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The public route of a Nuxt Content page, from its content id. Nuxt Content
|
|
5
|
+
* drops the number prefixes of the folders and files, and a final `index`
|
|
6
|
+
* segment. For example `1.getting-started/2.installation` gives
|
|
7
|
+
* `/getting-started/installation`, and `index` gives `/`. `prefix` is added in
|
|
8
|
+
* front of the path (for example `/docs`).
|
|
9
|
+
*/
|
|
10
|
+
export function contentRoutePath(id: string, prefix = ''): string {
|
|
11
|
+
const segments = String(id)
|
|
12
|
+
.split('/')
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map((segment) => segment.replace(NUMBER_PREFIX, ''))
|
|
15
|
+
|
|
16
|
+
if (segments.at(-1) === 'index') {
|
|
17
|
+
segments.pop()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const base = prefix.split('/').filter(Boolean)
|
|
21
|
+
const path = [...base, ...segments].join('/')
|
|
22
|
+
return `/${path}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The admin form of a content item: `/admin/content/<type>/<id>`. Each segment
|
|
27
|
+
* of the id is URL-encoded, and the slashes stay (like `contentIdPath` in core).
|
|
28
|
+
*/
|
|
29
|
+
export function contentEditPath(type: string, id: string): string {
|
|
30
|
+
const path = String(id)
|
|
31
|
+
.split('/')
|
|
32
|
+
.map((segment) => encodeURIComponent(segment))
|
|
33
|
+
.join('/')
|
|
34
|
+
return `/admin/content/${encodeURIComponent(type)}/${path}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The admin form that creates an item of a type: `/admin/content/<type>/new`. */
|
|
38
|
+
export function contentNewPath(type: string): string {
|
|
39
|
+
return `/admin/content/${encodeURIComponent(type)}/new`
|
|
40
|
+
}
|