@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.
Files changed (61) hide show
  1. package/.gitattributes +1 -0
  2. package/.prettierignore +5 -0
  3. package/CHANGELOG.md +178 -0
  4. package/FEATURES.md +30 -0
  5. package/README.md +39 -2
  6. package/app/assets/css/pluto-layer.css +40 -0
  7. package/app/components/Pluto/Docs/Blocks/PlutoBlockEditor.vue +163 -0
  8. package/app/components/Pluto/Docs/Blocks/PlutoBlockInspector.vue +519 -0
  9. package/app/components/Pluto/Docs/Blocks/PlutoBlockMarkdownSplit.vue +159 -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 +85 -0
  14. package/app/components/Pluto/Docs/Blocks/PlutoBlockPreviewFrame.vue +233 -0
  15. package/app/components/Pluto/Docs/Blocks/PlutoBlockPropControl.vue +246 -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 +443 -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/index.ts +123 -0
  31. package/modules/pluto-docs/manifest.ts +159 -0
  32. package/modules/pluto-docs/options.ts +224 -0
  33. package/nuxt.config.ts +36 -0
  34. package/package.json +55 -4
  35. package/server/handlers/navigation.get.ts +7 -0
  36. package/server/handlers/page.get.ts +7 -0
  37. package/server/handlers/surround.get.ts +7 -0
  38. package/server/plugins/pluto-docs.ts +25 -0
  39. package/server/utils/pluto-docs-runtime.ts +504 -0
  40. package/shared/types/docs.d.ts +40 -0
  41. package/shared/types/pluto-manifest.d.ts +6 -0
  42. package/shared/utils/blocks/editor.ts +357 -0
  43. package/shared/utils/blocks/history.ts +94 -0
  44. package/shared/utils/blocks/mdc-props.ts +321 -0
  45. package/shared/utils/blocks/operations.ts +692 -0
  46. package/shared/utils/blocks/parse.ts +444 -0
  47. package/shared/utils/blocks/presets.ts +383 -0
  48. package/shared/utils/blocks/preview-marks.ts +98 -0
  49. package/shared/utils/blocks/preview-protocol.ts +195 -0
  50. package/shared/utils/blocks/registry.ts +163 -0
  51. package/shared/utils/blocks/serialize.ts +107 -0
  52. package/shared/utils/blocks/tree.ts +85 -0
  53. package/shared/utils/blocks/types.ts +165 -0
  54. package/shared/utils/content-route.ts +40 -0
  55. package/shared/utils/docs-type.ts +290 -0
  56. package/shared/utils/mdc-snippets.ts +114 -0
  57. package/shared/utils/navigation-config.ts +19 -0
  58. package/shared/utils/runtime-content.ts +403 -0
  59. package/tailwind.lint.css +10 -0
  60. package/tsconfig.json +18 -0
  61. 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
+ }