@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,237 @@
|
|
|
1
|
+
// The read side of the public pages. These composables hide the render mode
|
|
2
|
+
// (`plutoDocs.render`). In `'build'` mode they call Nuxt Content's
|
|
3
|
+
// `queryCollection`. In `'hybrid'` and `'runtime'` mode they call the layer's server routes:
|
|
4
|
+
// `'runtime'` reads the Markdown from the content repository, and `'hybrid'` serves the build
|
|
5
|
+
// data until a file differs from the build. A site calls only these, so a switch
|
|
6
|
+
// of the mode needs no change in its pages. See the docs-setup skill.
|
|
7
|
+
|
|
8
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A page of the public site. In `'build'` mode it is a Nuxt Content page. In the other
|
|
12
|
+
* modes it has the same fields that the pages use: `id`, `stem`, `extension`, `path`,
|
|
13
|
+
* `title`, `description`, `seo`, `navigation`, `meta`, the other frontmatter keys and
|
|
14
|
+
* `body` (with `body.toc`). Pass it to `ContentRenderer`.
|
|
15
|
+
*/
|
|
16
|
+
export interface PlutoContentPage {
|
|
17
|
+
id: string
|
|
18
|
+
stem: string
|
|
19
|
+
extension: string
|
|
20
|
+
path: string
|
|
21
|
+
title: string
|
|
22
|
+
description: string
|
|
23
|
+
seo?: { title?: string; description?: string }
|
|
24
|
+
navigation?: false | { title?: string; icon?: string }
|
|
25
|
+
|
|
26
|
+
body: any
|
|
27
|
+
|
|
28
|
+
[key: string]: any
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A node of the navigation tree. The shape of `ContentNavigationItem` of Nuxt Content. */
|
|
32
|
+
export interface PlutoContentNavigationItem {
|
|
33
|
+
title: string
|
|
34
|
+
path: string
|
|
35
|
+
stem?: string
|
|
36
|
+
children?: PlutoContentNavigationItem[]
|
|
37
|
+
page?: false
|
|
38
|
+
icon?: string
|
|
39
|
+
|
|
40
|
+
[key: string]: any
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The previous or the next page. Like Nuxt Content, the type has no `null`, but an end of the list is `null` at run time. */
|
|
44
|
+
export interface PlutoContentSurroundItem {
|
|
45
|
+
title: string
|
|
46
|
+
path: string
|
|
47
|
+
description?: string
|
|
48
|
+
|
|
49
|
+
[key: string]: any
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PlutoContentReadOptions {
|
|
53
|
+
/**
|
|
54
|
+
* The Nuxt Content collection to query in `'build'` and `'hybrid'` mode, for example `docs`.
|
|
55
|
+
* Required there. The collection config of the site decides which files it holds.
|
|
56
|
+
*/
|
|
57
|
+
collection?: string
|
|
58
|
+
/** The content type to read in `'hybrid'` and `'runtime'` mode. Default: the first type of the layer. */
|
|
59
|
+
type?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PlutoContentNavigationOptions extends PlutoContentReadOptions {
|
|
63
|
+
/**
|
|
64
|
+
* `'hybrid'` and `'runtime'` mode only: keep the root `index` page in the tree when the tree
|
|
65
|
+
* comes from the store. Default false, because the root page is the landing page. In `'build'`
|
|
66
|
+
* mode, and in `'hybrid'` mode while the build data answers, the collection decides.
|
|
67
|
+
*/
|
|
68
|
+
includeRoot?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `'hybrid'`, `'build'` or `'runtime'`: the render mode of the site. */
|
|
72
|
+
export function usePlutoContentRender(): 'hybrid' | 'build' | 'runtime' {
|
|
73
|
+
const render = useRuntimeConfig().public.plutoDocs?.render
|
|
74
|
+
return render === 'runtime' || render === 'hybrid' ? render : 'build'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function runtimeTypeName(options: PlutoContentReadOptions): string {
|
|
78
|
+
const config = useRuntimeConfig().public.plutoDocs as unknown as
|
|
79
|
+
{ types?: Array<{ name: string }> } | undefined
|
|
80
|
+
const name = options.type ?? config?.types?.[0]?.name
|
|
81
|
+
if (!name) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'[pluto-docs] No content type is registered. Add one to plutoDocs.types.'
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
return name
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildCollection(
|
|
90
|
+
options: PlutoContentReadOptions,
|
|
91
|
+
caller: string
|
|
92
|
+
): string {
|
|
93
|
+
if (!options.collection) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`[pluto-docs] ${caller} needs the "collection" option in build mode.`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
return options.collection
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizePath(path: string): string {
|
|
102
|
+
return path.length > 1 ? path.replace(/\/+$/, '') : path
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A route of the layer on the public API of Pluto. See `RUNTIME_ROUTE_BASE` in the module. */
|
|
106
|
+
function routeUrl(
|
|
107
|
+
type: string,
|
|
108
|
+
name: 'page' | 'navigation' | 'surround'
|
|
109
|
+
): string {
|
|
110
|
+
return `/api/pluto/v1/docs/${encodeURIComponent(type)}/${name}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Runs a server route and returns its `data`. A 404 gives `null`. Any other error is thrown. */
|
|
114
|
+
async function fetchRoute<T>(
|
|
115
|
+
url: string,
|
|
116
|
+
query: Record<string, string | undefined>
|
|
117
|
+
): Promise<T | null> {
|
|
118
|
+
try {
|
|
119
|
+
const answer = await $fetch<{ data: T }>(url, { query })
|
|
120
|
+
return answer.data
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if ((error as { statusCode?: number }).statusCode === 404) {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
throw error
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The page at a route path, or `null` when there is none. Returns the result of
|
|
131
|
+
* `useAsyncData`, so `await` it in `<script setup>`. The path follows the ref.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* const { data: page } = await usePlutoContentPage(() => route.path, { collection: 'docs' })
|
|
135
|
+
*/
|
|
136
|
+
export function usePlutoContentPage(
|
|
137
|
+
path: MaybeRefOrGetter<string>,
|
|
138
|
+
options: PlutoContentReadOptions = {}
|
|
139
|
+
) {
|
|
140
|
+
const mode = usePlutoContentRender()
|
|
141
|
+
const key = computed(
|
|
142
|
+
() =>
|
|
143
|
+
`pluto-content-page:${mode}:${options.collection ?? options.type ?? ''}:${normalizePath(toValue(path))}`
|
|
144
|
+
)
|
|
145
|
+
return useAsyncData<PlutoContentPage | null>(
|
|
146
|
+
() => key.value,
|
|
147
|
+
async () => {
|
|
148
|
+
const current = normalizePath(toValue(path))
|
|
149
|
+
if (mode !== 'build') {
|
|
150
|
+
return fetchRoute<PlutoContentPage>(
|
|
151
|
+
routeUrl(runtimeTypeName(options), 'page'),
|
|
152
|
+
{ path: current, collection: options.collection }
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
// The collection name is a string here. The generated types know the names of the site.
|
|
156
|
+
const page = await queryCollection(
|
|
157
|
+
buildCollection(options, 'usePlutoContentPage') as never
|
|
158
|
+
)
|
|
159
|
+
.path(current)
|
|
160
|
+
.first()
|
|
161
|
+
return (page as PlutoContentPage | null) ?? null
|
|
162
|
+
},
|
|
163
|
+
{ watch: [key] }
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The navigation tree. The shape is the same in both modes, so it goes to `UContentNavigation`,
|
|
169
|
+
* `UContentSearch` and `findPageHeadline`.
|
|
170
|
+
*
|
|
171
|
+
* Folder titles, icons and the other keys of a `.navigation.yml` file apply in every mode: Nuxt
|
|
172
|
+
* Content reads them at build time, and the server reads them from the store at run time.
|
|
173
|
+
*/
|
|
174
|
+
export function usePlutoContentNavigation(
|
|
175
|
+
options: PlutoContentNavigationOptions = {}
|
|
176
|
+
) {
|
|
177
|
+
const mode = usePlutoContentRender()
|
|
178
|
+
return useAsyncData<PlutoContentNavigationItem[]>(
|
|
179
|
+
`pluto-content-navigation:${mode}:${options.collection ?? options.type ?? ''}`,
|
|
180
|
+
async () => {
|
|
181
|
+
if (mode !== 'build') {
|
|
182
|
+
const tree = await fetchRoute<PlutoContentNavigationItem[]>(
|
|
183
|
+
routeUrl(runtimeTypeName(options), 'navigation'),
|
|
184
|
+
{
|
|
185
|
+
collection: options.collection,
|
|
186
|
+
includeRoot: options.includeRoot ? '1' : undefined,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
return tree ?? []
|
|
190
|
+
}
|
|
191
|
+
return (await queryCollectionNavigation(
|
|
192
|
+
buildCollection(options, 'usePlutoContentNavigation') as never
|
|
193
|
+
)) as PlutoContentNavigationItem[]
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The previous and the next page of a route path: `[previous, next]`, each one an item or
|
|
200
|
+
* `null`. Each item has `title`, `path` and `description`.
|
|
201
|
+
*/
|
|
202
|
+
export function usePlutoContentSurround(
|
|
203
|
+
path: MaybeRefOrGetter<string>,
|
|
204
|
+
options: PlutoContentNavigationOptions = {}
|
|
205
|
+
) {
|
|
206
|
+
const mode = usePlutoContentRender()
|
|
207
|
+
const key = computed(
|
|
208
|
+
() =>
|
|
209
|
+
`pluto-content-surround:${mode}:${options.collection ?? options.type ?? ''}:${normalizePath(toValue(path))}`
|
|
210
|
+
)
|
|
211
|
+
return useAsyncData<PlutoContentSurroundItem[]>(
|
|
212
|
+
() => key.value,
|
|
213
|
+
async () => {
|
|
214
|
+
const current = normalizePath(toValue(path))
|
|
215
|
+
if (mode !== 'build') {
|
|
216
|
+
const surround = await fetchRoute<PlutoContentSurroundItem[]>(
|
|
217
|
+
routeUrl(runtimeTypeName(options), 'surround'),
|
|
218
|
+
{
|
|
219
|
+
path: current,
|
|
220
|
+
collection: options.collection,
|
|
221
|
+
includeRoot: options.includeRoot ? '1' : undefined,
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
return surround ?? []
|
|
225
|
+
}
|
|
226
|
+
const surround = await queryCollectionItemSurroundings(
|
|
227
|
+
buildCollection(options, 'usePlutoContentSurround') as never,
|
|
228
|
+
current,
|
|
229
|
+
{
|
|
230
|
+
fields: ['description'],
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
return surround as PlutoContentSurroundItem[]
|
|
234
|
+
},
|
|
235
|
+
{ watch: [key] }
|
|
236
|
+
)
|
|
237
|
+
}
|