@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.
- package/.gitattributes +1 -0
- package/.prettierignore +5 -0
- package/CHANGELOG.md +186 -0
- package/FEATURES.md +34 -0
- package/README.md +40 -2
- package/app/assets/css/pluto-layer.css +40 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +165 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockInspector.vue +519 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +161 -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 +86 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +232 -0
- package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +269 -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 +459 -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/blocks.ts +217 -0
- package/modules/pluto-docs/index.ts +230 -0
- package/modules/pluto-docs/manifest.ts +159 -0
- package/modules/pluto-docs/options.ts +258 -0
- package/nuxt.config.ts +36 -0
- package/package.json +56 -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-blocks.d.ts +10 -0
- package/shared/types/pluto-manifest.d.ts +6 -0
- package/shared/utils/blocks/component-meta.ts +285 -0
- package/shared/utils/blocks/define.ts +15 -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 +384 -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 +295 -0
- package/shared/utils/blocks/serialize.ts +107 -0
- package/shared/utils/blocks/tree.ts +85 -0
- package/shared/utils/blocks/types.ts +195 -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,459 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey, Ref } from 'vue'
|
|
2
|
+
import type { PlutoPaletteEntry } from '../../shared/utils/blocks/editor'
|
|
3
|
+
import type { PlutoHistory } from '../../shared/utils/blocks/history'
|
|
4
|
+
import type { PlutoBlockTarget } from '../../shared/utils/blocks/operations'
|
|
5
|
+
import type {
|
|
6
|
+
PlutoBlock,
|
|
7
|
+
PlutoBlockDefinition,
|
|
8
|
+
PlutoBlockDoc,
|
|
9
|
+
PlutoBlockLocation,
|
|
10
|
+
PlutoBlocksConfig,
|
|
11
|
+
PlutoBlockSources,
|
|
12
|
+
PlutoCodeBlock,
|
|
13
|
+
PlutoPropValue,
|
|
14
|
+
} from '../../shared/utils/blocks/types'
|
|
15
|
+
import { computed, inject, ref, shallowRef, watch } from 'vue'
|
|
16
|
+
import { acceptedNames, describeBlock } from '../../shared/utils/blocks/editor'
|
|
17
|
+
import {
|
|
18
|
+
canRedo,
|
|
19
|
+
canUndo,
|
|
20
|
+
createHistory,
|
|
21
|
+
pushHistory,
|
|
22
|
+
redo,
|
|
23
|
+
resetHistory,
|
|
24
|
+
undo,
|
|
25
|
+
} from '../../shared/utils/blocks/history'
|
|
26
|
+
import {
|
|
27
|
+
blocksFromMarkdown,
|
|
28
|
+
duplicateBlock,
|
|
29
|
+
insertBlocks,
|
|
30
|
+
moveBlock,
|
|
31
|
+
removeBlock,
|
|
32
|
+
replaceWithMarkdown,
|
|
33
|
+
setProp,
|
|
34
|
+
setSlot,
|
|
35
|
+
updateCode,
|
|
36
|
+
updateText,
|
|
37
|
+
} from '../../shared/utils/blocks/operations'
|
|
38
|
+
import { parseBlocks } from '../../shared/utils/blocks/parse'
|
|
39
|
+
import {
|
|
40
|
+
humanizeName,
|
|
41
|
+
resolveBlockDefinition,
|
|
42
|
+
} from '../../shared/utils/blocks/registry'
|
|
43
|
+
import { serializeBlocks } from '../../shared/utils/blocks/serialize'
|
|
44
|
+
import { findBlock, flatChildren, pathOf } from '../../shared/utils/blocks/tree'
|
|
45
|
+
|
|
46
|
+
export interface PlutoBlockSelection {
|
|
47
|
+
id: string
|
|
48
|
+
/** `preview` when a click in the preview selected the block, so the preview needs no scroll. */
|
|
49
|
+
source: 'ui' | 'preview'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PlutoBlockEditorOptions {
|
|
53
|
+
/** The Markdown text of the field. */
|
|
54
|
+
source: () => string
|
|
55
|
+
/** Called with the new text after each edit, undo and redo. Never called at the start. */
|
|
56
|
+
emit: (value: string) => void
|
|
57
|
+
/** The layers of the block definitions: the component layer and the `plutoBlocks` of the app config. */
|
|
58
|
+
sources?: () => PlutoBlockSources | undefined
|
|
59
|
+
now?: () => number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PlutoBlockEditor {
|
|
63
|
+
/** The layers of the block definitions. The palette, the preview and the labels read them here. */
|
|
64
|
+
sources: ComputedRef<PlutoBlockSources>
|
|
65
|
+
doc: ComputedRef<PlutoBlockDoc>
|
|
66
|
+
/** The message of a parse error. The editor then offers the Markdown mode only. */
|
|
67
|
+
error: Ref<string | null>
|
|
68
|
+
selection: Ref<PlutoBlockSelection | null>
|
|
69
|
+
selectedId: ComputedRef<string | null>
|
|
70
|
+
selected: ComputedRef<PlutoBlockLocation | undefined>
|
|
71
|
+
hoverId: Ref<string | null>
|
|
72
|
+
canUndo: ComputedRef<boolean>
|
|
73
|
+
canRedo: ComputedRef<boolean>
|
|
74
|
+
/** True once a block was edited since the editor opened. */
|
|
75
|
+
edited: ComputedRef<boolean>
|
|
76
|
+
/** The ids of the blocks that changed since the editor opened. A dot marks them in the outline. */
|
|
77
|
+
editedIds: ComputedRef<Set<string>>
|
|
78
|
+
palette: Ref<{ open: boolean; target: PlutoBlockTarget | null }>
|
|
79
|
+
/** The components whose children the outline hides. A selection inside one opens it. */
|
|
80
|
+
collapsedIds: Ref<Set<string>>
|
|
81
|
+
toggleCollapsed: (id: string) => void
|
|
82
|
+
select: (id: string | null, source?: 'ui' | 'preview') => void
|
|
83
|
+
openPalette: (target?: PlutoBlockTarget | null) => void
|
|
84
|
+
undo: () => void
|
|
85
|
+
redo: () => void
|
|
86
|
+
insert: (
|
|
87
|
+
entry: PlutoPaletteEntry,
|
|
88
|
+
target?: PlutoBlockTarget | null
|
|
89
|
+
) => string[]
|
|
90
|
+
remove: (id: string) => void
|
|
91
|
+
duplicate: (id: string) => void
|
|
92
|
+
/** Moves a block by `delta` places in its own list. */
|
|
93
|
+
move: (id: string, delta: number) => void
|
|
94
|
+
moveTo: (id: string, target: PlutoBlockTarget) => void
|
|
95
|
+
setProp: (id: string, key: string, value: PlutoPropValue) => void
|
|
96
|
+
setSlot: (id: string, slot: string, markdown: string) => void
|
|
97
|
+
updateText: (id: string, markdown: string) => void
|
|
98
|
+
updateCode: (
|
|
99
|
+
id: string,
|
|
100
|
+
patch: Partial<Pick<PlutoCodeBlock, 'lang' | 'filename' | 'meta' | 'code'>>
|
|
101
|
+
) => void
|
|
102
|
+
/** Replaces a block with the blocks parsed from Markdown, and selects the first one. */
|
|
103
|
+
replaceMarkdown: (id: string, markdown: string) => void
|
|
104
|
+
definitionOf: (block: PlutoBlock) => PlutoBlockDefinition | undefined
|
|
105
|
+
describe: (block: PlutoBlock) => ReturnType<typeof describeBlock>
|
|
106
|
+
/** The list that holds a block: the doc, or one slot. */
|
|
107
|
+
siblings: (id: string) => PlutoBlock[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const plutoBlockEditorKey: InjectionKey<PlutoBlockEditor> =
|
|
111
|
+
Symbol('plutoBlockEditor')
|
|
112
|
+
|
|
113
|
+
/** The block editor that the calling component sits in. Throws outside a `PlutoBlockEditor`. */
|
|
114
|
+
export function usePlutoBlockEditor(): PlutoBlockEditor {
|
|
115
|
+
const editor = inject(plutoBlockEditorKey, null)
|
|
116
|
+
if (!editor) {
|
|
117
|
+
throw new Error('usePlutoBlockEditor() needs a PlutoBlockEditor above it.')
|
|
118
|
+
}
|
|
119
|
+
return editor
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function tryParse(source: string): {
|
|
123
|
+
doc: PlutoBlockDoc
|
|
124
|
+
error: string | null
|
|
125
|
+
} {
|
|
126
|
+
try {
|
|
127
|
+
return { doc: parseBlocks(source), error: null }
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return {
|
|
130
|
+
doc: { blocks: [], gaps: [source] },
|
|
131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Ids of the blocks whose `original` is gone, over the whole tree. */
|
|
137
|
+
function editedBlockIds(
|
|
138
|
+
blocks: PlutoBlock[],
|
|
139
|
+
out = new Set<string>()
|
|
140
|
+
): Set<string> {
|
|
141
|
+
for (const block of blocks) {
|
|
142
|
+
if (!block.original) {
|
|
143
|
+
out.add(block.id)
|
|
144
|
+
}
|
|
145
|
+
if (block.kind === 'component') {
|
|
146
|
+
editedBlockIds(flatChildren(block), out)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The state of one block editor: the doc, the selection, the undo history and
|
|
154
|
+
* the edit actions. Each edit builds a new doc, writes the text and calls
|
|
155
|
+
* `options.emit`. A block that no edit touched keeps its source bytes, so
|
|
156
|
+
* opening a page and leaving it changes nothing.
|
|
157
|
+
*/
|
|
158
|
+
export function createPlutoBlockEditor(
|
|
159
|
+
options: PlutoBlockEditorOptions
|
|
160
|
+
): PlutoBlockEditor {
|
|
161
|
+
const now = options.now ?? Date.now
|
|
162
|
+
const sources = computed(() => options.sources?.() ?? {})
|
|
163
|
+
|
|
164
|
+
const initial = tryParse(options.source())
|
|
165
|
+
const error = ref<string | null>(initial.error)
|
|
166
|
+
const history = shallowRef<PlutoHistory<PlutoBlockDoc>>(
|
|
167
|
+
createHistory(initial.doc)
|
|
168
|
+
)
|
|
169
|
+
const doc = computed(() => history.value.present)
|
|
170
|
+
const selection = ref<PlutoBlockSelection | null>(null)
|
|
171
|
+
const hoverId = ref<string | null>(null)
|
|
172
|
+
const palette = ref<{ open: boolean; target: PlutoBlockTarget | null }>({
|
|
173
|
+
open: false,
|
|
174
|
+
target: null,
|
|
175
|
+
})
|
|
176
|
+
const collapsedIds = ref(new Set<string>())
|
|
177
|
+
let lastEmitted = options.source()
|
|
178
|
+
|
|
179
|
+
const selected = computed(() =>
|
|
180
|
+
selection.value ? findBlock(doc.value, selection.value.id) : undefined
|
|
181
|
+
)
|
|
182
|
+
const selectedId = computed(() => selected.value?.block.id ?? null)
|
|
183
|
+
|
|
184
|
+
// The text changed outside the editor (a reload, a switch from the Markdown mode).
|
|
185
|
+
watch(options.source, (value) => {
|
|
186
|
+
if (value === lastEmitted) {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
const parsed = tryParse(value)
|
|
190
|
+
error.value = parsed.error
|
|
191
|
+
history.value = resetHistory(parsed.doc)
|
|
192
|
+
lastEmitted = value
|
|
193
|
+
selection.value = null
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const editedIds = computed(() => editedBlockIds(doc.value.blocks))
|
|
197
|
+
|
|
198
|
+
function toggleCollapsed(id: string): void {
|
|
199
|
+
const next = new Set(collapsedIds.value)
|
|
200
|
+
if (!next.delete(id)) {
|
|
201
|
+
next.add(id)
|
|
202
|
+
}
|
|
203
|
+
collapsedIds.value = next
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// A block selected in the preview or in the panel must show in the outline: open its parents.
|
|
207
|
+
watch(selection, (current) => {
|
|
208
|
+
if (!current) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
const parents = pathOf(doc.value, current.id).slice(0, -1)
|
|
212
|
+
if (parents.some((id) => collapsedIds.value.has(id))) {
|
|
213
|
+
collapsedIds.value = new Set(
|
|
214
|
+
[...collapsedIds.value].filter((id) => !parents.includes(id))
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
function write(next: PlutoBlockDoc): void {
|
|
220
|
+
const text = serializeBlocks(next)
|
|
221
|
+
lastEmitted = text
|
|
222
|
+
options.emit(text)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function commit(next: PlutoBlockDoc, key?: string): boolean {
|
|
226
|
+
if (next === doc.value) {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
history.value = pushHistory(history.value, next, now(), key)
|
|
230
|
+
write(next)
|
|
231
|
+
return true
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function select(id: string | null, source: 'ui' | 'preview' = 'ui'): void {
|
|
235
|
+
selection.value = id ? { id, source } : null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function siblings(id: string): PlutoBlock[] {
|
|
239
|
+
const found = findBlock(doc.value, id)
|
|
240
|
+
if (!found) {
|
|
241
|
+
return []
|
|
242
|
+
}
|
|
243
|
+
return found.parent
|
|
244
|
+
? (found.parent.slots[found.slot ?? 'default'] ?? [])
|
|
245
|
+
: doc.value.blocks
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function definitionOf(block: PlutoBlock): PlutoBlockDefinition | undefined {
|
|
249
|
+
return block.kind === 'component'
|
|
250
|
+
? resolveBlockDefinition(block.name, { sources: sources.value, block })
|
|
251
|
+
: undefined
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** After the selected block, or into a slot of the selected block that accepts the new component. */
|
|
255
|
+
function defaultTarget(entry: PlutoPaletteEntry): PlutoBlockTarget {
|
|
256
|
+
const current = selected.value
|
|
257
|
+
if (!current) {
|
|
258
|
+
return { index: doc.value.blocks.length }
|
|
259
|
+
}
|
|
260
|
+
if (entry.kind === 'component' && current.block.kind === 'component') {
|
|
261
|
+
const definition = definitionOf(current.block)
|
|
262
|
+
for (const slot of Object.keys(definition?.slots ?? {})) {
|
|
263
|
+
const accepts = definition ? acceptedNames(definition, slot) : undefined
|
|
264
|
+
if (accepts?.includes(entry.name)) {
|
|
265
|
+
return {
|
|
266
|
+
parentId: current.block.id,
|
|
267
|
+
slot,
|
|
268
|
+
index: (current.block.slots[slot] ?? []).length,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
parentId: current.parent?.id,
|
|
275
|
+
slot: current.slot,
|
|
276
|
+
index: current.index + 1,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function insert(
|
|
281
|
+
entry: PlutoPaletteEntry,
|
|
282
|
+
target?: PlutoBlockTarget | null
|
|
283
|
+
): string[] {
|
|
284
|
+
const parsed = blocksFromMarkdown(entry.markdown)
|
|
285
|
+
const result = insertBlocks(
|
|
286
|
+
doc.value,
|
|
287
|
+
parsed.blocks,
|
|
288
|
+
target ?? defaultTarget(entry),
|
|
289
|
+
parsed.gaps
|
|
290
|
+
)
|
|
291
|
+
if (!result) {
|
|
292
|
+
return []
|
|
293
|
+
}
|
|
294
|
+
commit(result.doc)
|
|
295
|
+
if (result.ids[0]) {
|
|
296
|
+
select(result.ids[0])
|
|
297
|
+
}
|
|
298
|
+
return result.ids
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function remove(id: string): void {
|
|
302
|
+
const found = findBlock(doc.value, id)
|
|
303
|
+
if (!found) {
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
const list = siblings(id)
|
|
307
|
+
const neighbour =
|
|
308
|
+
list[found.index + 1] ?? list[found.index - 1] ?? found.parent
|
|
309
|
+
commit(removeBlock(doc.value, id))
|
|
310
|
+
select(neighbour?.id ?? null)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function duplicate(id: string): void {
|
|
314
|
+
const result = duplicateBlock(doc.value, id)
|
|
315
|
+
if (result) {
|
|
316
|
+
commit(result.doc)
|
|
317
|
+
select(result.ids[0] ?? null)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function moveTo(id: string, target: PlutoBlockTarget): void {
|
|
322
|
+
commit(moveBlock(doc.value, id, target))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function move(id: string, delta: number): void {
|
|
326
|
+
const found = findBlock(doc.value, id)
|
|
327
|
+
if (!found) {
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
const length = siblings(id).length
|
|
331
|
+
const index = Math.max(0, Math.min(found.index + delta, length - 1))
|
|
332
|
+
if (index !== found.index) {
|
|
333
|
+
moveTo(id, { parentId: found.parent?.id, slot: found.slot, index })
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
sources,
|
|
339
|
+
doc,
|
|
340
|
+
error,
|
|
341
|
+
selection,
|
|
342
|
+
selectedId,
|
|
343
|
+
selected,
|
|
344
|
+
hoverId,
|
|
345
|
+
canUndo: computed(() => canUndo(history.value)),
|
|
346
|
+
canRedo: computed(() => canRedo(history.value)),
|
|
347
|
+
edited: computed(() => editedIds.value.size > 0),
|
|
348
|
+
editedIds,
|
|
349
|
+
palette,
|
|
350
|
+
collapsedIds,
|
|
351
|
+
toggleCollapsed,
|
|
352
|
+
select,
|
|
353
|
+
openPalette(target = null) {
|
|
354
|
+
palette.value = { open: true, target }
|
|
355
|
+
},
|
|
356
|
+
undo() {
|
|
357
|
+
history.value = undo(history.value)
|
|
358
|
+
write(history.value.present)
|
|
359
|
+
},
|
|
360
|
+
redo() {
|
|
361
|
+
history.value = redo(history.value)
|
|
362
|
+
write(history.value.present)
|
|
363
|
+
},
|
|
364
|
+
insert,
|
|
365
|
+
remove,
|
|
366
|
+
duplicate,
|
|
367
|
+
move,
|
|
368
|
+
moveTo,
|
|
369
|
+
setProp: (id, key, value) =>
|
|
370
|
+
void commit(setProp(doc.value, id, key, value), `prop:${id}:${key}`),
|
|
371
|
+
setSlot: (id, slot, markdown) =>
|
|
372
|
+
void commit(setSlot(doc.value, id, slot, markdown), `slot:${id}:${slot}`),
|
|
373
|
+
updateText: (id, markdown) =>
|
|
374
|
+
void commit(updateText(doc.value, id, markdown), `text:${id}`),
|
|
375
|
+
updateCode: (id, patch) =>
|
|
376
|
+
void commit(updateCode(doc.value, id, patch), `code:${id}`),
|
|
377
|
+
replaceMarkdown(id, markdown) {
|
|
378
|
+
const result = replaceWithMarkdown(doc.value, id, markdown)
|
|
379
|
+
if (result) {
|
|
380
|
+
commit(result.doc)
|
|
381
|
+
select(result.ids[0] ?? null)
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
definitionOf,
|
|
385
|
+
describe: (block) => describeBlock(block, sources.value),
|
|
386
|
+
siblings,
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** The id of the top-level block that holds a block. The preview selects at this level. */
|
|
391
|
+
export function topLevelId(
|
|
392
|
+
doc: PlutoBlockDoc,
|
|
393
|
+
id: string | null
|
|
394
|
+
): string | null {
|
|
395
|
+
return id ? (pathOf(doc, id)[0] ?? null) : null
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The `plutoBlocks` value of the app config, as `PlutoBlocksConfig`. Nuxt types
|
|
400
|
+
* `useAppConfig()` from the literal config of the site, where `'inline'` is a
|
|
401
|
+
* `string`, so read the value through this function, not from `useAppConfig()`.
|
|
402
|
+
*/
|
|
403
|
+
export function usePlutoBlocksConfig(): () => PlutoBlocksConfig | undefined {
|
|
404
|
+
const appConfig = useAppConfig()
|
|
405
|
+
return () => appConfig.plutoBlocks as unknown as PlutoBlocksConfig | undefined
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The layers of the block definitions for a block editor: the component layer and the
|
|
410
|
+
* `plutoBlocks` value of the app config. The caller passes the component layer in: the two editor
|
|
411
|
+
* roots import it from `#pluto-docs/blocks`, so the data stays in the lazy editor chunk.
|
|
412
|
+
*/
|
|
413
|
+
export function usePlutoBlockSources(
|
|
414
|
+
components: PlutoBlocksConfig = {}
|
|
415
|
+
): () => PlutoBlockSources {
|
|
416
|
+
const blocksConfig = usePlutoBlocksConfig()
|
|
417
|
+
return () => ({ components, overrides: blocksConfig() })
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** The texts of the slots of a component: the title of a slot, and the label of its "Add" button. */
|
|
421
|
+
export function usePlutoBlockLabels() {
|
|
422
|
+
const { t, tx } = usePlutoI18n()
|
|
423
|
+
const editor = usePlutoBlockEditor()
|
|
424
|
+
|
|
425
|
+
function slotLabel(
|
|
426
|
+
definition: PlutoBlockDefinition | undefined,
|
|
427
|
+
slot: string
|
|
428
|
+
): string {
|
|
429
|
+
const label = definition?.slots?.[slot]?.label
|
|
430
|
+
if (label) {
|
|
431
|
+
return tx(label)
|
|
432
|
+
}
|
|
433
|
+
return slot === 'default'
|
|
434
|
+
? t('docs.blocks.slots.default')
|
|
435
|
+
: humanizeName(slot)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** "Add Card" when the slot takes one kind of block, "Add a block" for the main slot, else "Add to Buttons". */
|
|
439
|
+
function addLabel(
|
|
440
|
+
definition: PlutoBlockDefinition | undefined,
|
|
441
|
+
slot: string
|
|
442
|
+
): string {
|
|
443
|
+
const accepts = definition?.slots?.[slot]?.accepts
|
|
444
|
+
if (accepts?.length === 1) {
|
|
445
|
+
return t('docs.editor.addItem', {
|
|
446
|
+
name: tx(
|
|
447
|
+
resolveBlockDefinition(accepts[0]!, { sources: editor.sources.value })
|
|
448
|
+
.label
|
|
449
|
+
),
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
if (slot === 'default') {
|
|
453
|
+
return t('docs.editor.addBlock')
|
|
454
|
+
}
|
|
455
|
+
return t('docs.editor.addTo', { name: slotLabel(definition, slot) })
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return { slotLabel, addLabel }
|
|
459
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The admin edit link of a Nuxt Content page, for the signed-in staff only.
|
|
3
|
+
* Returns `null` for a visitor, so a public page shows no link. The link is
|
|
4
|
+
* `/admin/content/<type>/<id>`. The id is the page stem (`2.core/03.content-model`).
|
|
5
|
+
* The staff check runs in the browser, so the link appears after hydration,
|
|
6
|
+
* never in the server HTML.
|
|
7
|
+
*
|
|
8
|
+
* It also tells the admin bar which page is on screen, so the bar shows
|
|
9
|
+
* "Edit page" for it. It is a wrapper of core's `usePlutoEditLink`.
|
|
10
|
+
*
|
|
11
|
+
* @param page A Nuxt Content page. Only `stem` is read.
|
|
12
|
+
* @param type The content type name. Default `doc`.
|
|
13
|
+
*/
|
|
14
|
+
export function usePlutoContentEditLink(
|
|
15
|
+
page: MaybeRefOrGetter<{ stem?: string } | null | undefined>,
|
|
16
|
+
type = 'doc'
|
|
17
|
+
) {
|
|
18
|
+
return usePlutoEditLink(type, () => toValue(page)?.stem)
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { InjectionKey, MaybeRefOrGetter } from 'vue'
|
|
2
|
+
|
|
3
|
+
/** How the blocks render in the preview: the same options as the `ContentRenderer` of the site page. */
|
|
4
|
+
export interface PlutoPreviewRenderOptions {
|
|
5
|
+
/** `false` renders the HTML tags without the prose components, like `<ContentRenderer :prose="false">`. */
|
|
6
|
+
prose?: boolean
|
|
7
|
+
/** A map of tag to component, like the `components` prop of `ContentRenderer`. */
|
|
8
|
+
components?: Record<string, string>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const plutoPreviewRenderKey: InjectionKey<
|
|
12
|
+
MaybeRefOrGetter<PlutoPreviewRenderOptions>
|
|
13
|
+
> = Symbol('pluto-preview-render')
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sets how the blocks render in the preview of the visual editor. Call it in
|
|
17
|
+
* the site's own `PlutoContentPreview`, when a page renders with other
|
|
18
|
+
* `ContentRenderer` options than the default. For example, a landing page
|
|
19
|
+
* with `:prose="false"`:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* usePlutoPreviewRender(() => (props.frontmatter.id === 'index' ? { prose: false } : {}))
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function usePlutoPreviewRender(
|
|
26
|
+
options: MaybeRefOrGetter<PlutoPreviewRenderOptions>
|
|
27
|
+
) {
|
|
28
|
+
provide(plutoPreviewRenderKey, options)
|
|
29
|
+
}
|