@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,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
+ }