@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,321 @@
1
+ // Read and write the props of an MDC component: the inline form `{a="b" .c}`
2
+ // and the YAML block. A prop that did not change keeps its source text, so a
3
+ // small edit does not rewrite the other props.
4
+
5
+ import type { PlutoPropValue } from './types'
6
+ import { parseDocument, stringify } from 'yaml'
7
+
8
+ export interface PropSource {
9
+ value: PlutoPropValue
10
+ text: string
11
+ }
12
+
13
+ export interface ParsedProps {
14
+ props: Record<string, PlutoPropValue>
15
+ order: string[]
16
+ source: Record<string, PropSource>
17
+ }
18
+
19
+ export interface ParsedOpener {
20
+ colons: number
21
+ name: string
22
+ /** The text inside the braces, when the line has braces. */
23
+ braces?: string
24
+ }
25
+
26
+ const OPENER = /^(:{2,})([a-z][\w-]*)(?:\{(.*)\})?[ \t]*$/i
27
+
28
+ /** Reads the opening line of a container component. It returns `undefined` for a line it does not know. */
29
+ export function parseOpener(line: string): ParsedOpener | undefined {
30
+ const match = OPENER.exec(line)
31
+ if (!match) {
32
+ return undefined
33
+ }
34
+ return { colons: match[1]!.length, name: match[2]!, braces: match[3] }
35
+ }
36
+
37
+ export function isEqualValue(a: unknown, b: unknown): boolean {
38
+ if (a === b) {
39
+ return true
40
+ }
41
+ if (
42
+ typeof a !== 'object' ||
43
+ typeof b !== 'object' ||
44
+ a === null ||
45
+ b === null
46
+ ) {
47
+ return false
48
+ }
49
+ if (Array.isArray(a) !== Array.isArray(b)) {
50
+ return false
51
+ }
52
+ const keysA = Object.keys(a)
53
+ const keysB = Object.keys(b)
54
+ return (
55
+ keysA.length === keysB.length &&
56
+ keysA.every(
57
+ (key) =>
58
+ Object.hasOwn(b, key) &&
59
+ isEqualValue(
60
+ (a as Record<string, unknown>)[key],
61
+ (b as Record<string, unknown>)[key]
62
+ )
63
+ )
64
+ )
65
+ }
66
+
67
+ // Inline props
68
+
69
+ /** Splits the text inside the braces into tokens. A quoted value can hold spaces. */
70
+ function tokenize(body: string): string[] {
71
+ const tokens: string[] = []
72
+ let index = 0
73
+ while (index < body.length) {
74
+ if (/\s/.test(body[index]!)) {
75
+ index++
76
+ continue
77
+ }
78
+ const start = index
79
+ let quote = ''
80
+ while (index < body.length) {
81
+ const char = body[index]!
82
+ if (quote) {
83
+ if (char === '\\' && index + 1 < body.length) {
84
+ index++
85
+ } else if (char === quote) {
86
+ quote = ''
87
+ }
88
+ } else if (char === '"' || char === "'") {
89
+ quote = char
90
+ } else if (/\s/.test(char)) {
91
+ break
92
+ }
93
+ index++
94
+ }
95
+ tokens.push(body.slice(start, index))
96
+ }
97
+ return tokens
98
+ }
99
+
100
+ function unquote(raw: string): { text: string; quoted: boolean } {
101
+ const first = raw[0]
102
+ if (
103
+ (first === '"' || first === "'") &&
104
+ raw.length >= 2 &&
105
+ raw.endsWith(first)
106
+ ) {
107
+ return {
108
+ text: raw.slice(1, -1).replaceAll('&quot;', '"').replaceAll('&#39;', "'"),
109
+ quoted: true,
110
+ }
111
+ }
112
+ return { text: raw, quoted: false }
113
+ }
114
+
115
+ /** Reads the text inside the braces: `center .big #id title="A b" :cols="3"`. */
116
+ export function parseInlineProps(body: string): ParsedProps {
117
+ const result: ParsedProps = { props: {}, order: [], source: {} }
118
+ const add = (key: string, value: PlutoPropValue, text: string) => {
119
+ if (key in result.props) {
120
+ // `.a` and `class="b"` both set the class: join them.
121
+ if (
122
+ key === 'class' &&
123
+ typeof result.props[key] === 'string' &&
124
+ typeof value === 'string'
125
+ ) {
126
+ const joined = `${result.props[key]} ${value}`
127
+ result.props[key] = joined
128
+ result.source[key] = {
129
+ value: joined,
130
+ text: `${result.source[key]!.text} ${text}`,
131
+ }
132
+ return
133
+ }
134
+ // A repeated key: the last one wins, and the text of both stays.
135
+ result.source[key] = {
136
+ value,
137
+ text: `${result.source[key]!.text} ${text}`,
138
+ }
139
+ result.props[key] = value
140
+ return
141
+ }
142
+ result.props[key] = value
143
+ result.order.push(key)
144
+ result.source[key] = { value, text }
145
+ }
146
+
147
+ for (const token of tokenize(body)) {
148
+ if (token.startsWith('.') && token.length > 1) {
149
+ add('class', token.slice(1), token)
150
+ } else if (token.startsWith('#') && token.length > 1) {
151
+ add('id', token.slice(1), token)
152
+ } else {
153
+ const equals = token.indexOf('=')
154
+ if (equals === -1) {
155
+ add(token, true, token)
156
+ continue
157
+ }
158
+ const key = token.slice(0, equals)
159
+ const { text } = unquote(token.slice(equals + 1))
160
+ if (key.startsWith(':') && key.length > 1) {
161
+ try {
162
+ add(key.slice(1), JSON.parse(text), token)
163
+ continue
164
+ } catch {
165
+ // Not JSON: keep the key with its colon, as plain text.
166
+ }
167
+ }
168
+ add(key, text, token)
169
+ }
170
+ }
171
+ return result
172
+ }
173
+
174
+ function quoteString(value: string): string {
175
+ if (!value.includes('"')) {
176
+ return `"${value}"`
177
+ }
178
+ if (!value.includes("'")) {
179
+ return `'${value}'`
180
+ }
181
+ return `"${value.replaceAll('"', '&quot;')}"`
182
+ }
183
+
184
+ /** Writes one prop as an inline token. It returns `undefined` for a prop that must not show. */
185
+ export function renderInlineToken(
186
+ key: string,
187
+ value: PlutoPropValue
188
+ ): string | undefined {
189
+ if (value === undefined) {
190
+ return undefined
191
+ }
192
+ if (value === true) {
193
+ return key
194
+ }
195
+ if (typeof value === 'string') {
196
+ if (value.includes('\n')) {
197
+ return `:${key}='${JSON.stringify(value).replaceAll("'", '\\u0027')}'`
198
+ }
199
+ return `${key}=${quoteString(value)}`
200
+ }
201
+ return `:${key}='${JSON.stringify(value).replaceAll("'", '\\u0027')}'`
202
+ }
203
+
204
+ /** True when `props` still has the keys, the order and the values of the source. */
205
+ export function isSameProps(
206
+ props: Record<string, PlutoPropValue>,
207
+ order: string[],
208
+ source: Record<string, PropSource>
209
+ ): boolean {
210
+ const keys = order.filter((key) => props[key] !== undefined)
211
+ const sourceKeys = Object.keys(source)
212
+ if (
213
+ keys.length !== sourceKeys.length ||
214
+ Object.keys(props).filter((key) => props[key] !== undefined).length !==
215
+ keys.length
216
+ ) {
217
+ return false
218
+ }
219
+ return keys.every(
220
+ (key, index) =>
221
+ sourceKeys[index] === key && isEqualValue(props[key], source[key]!.value)
222
+ )
223
+ }
224
+
225
+ /** Writes the props as tokens, in order. An unchanged prop keeps its text. */
226
+ export function renderInlineProps(
227
+ props: Record<string, PlutoPropValue>,
228
+ order: string[],
229
+ source: Record<string, PropSource>
230
+ ): string {
231
+ const tokens: string[] = []
232
+ for (const key of order) {
233
+ const value = props[key]
234
+ if (value === undefined) {
235
+ continue
236
+ }
237
+ const known = source[key]
238
+ const token =
239
+ known && isEqualValue(known.value, value)
240
+ ? known.text
241
+ : renderInlineToken(key, value)
242
+ if (token) {
243
+ tokens.push(token)
244
+ }
245
+ }
246
+ return tokens.join(' ')
247
+ }
248
+
249
+ /** The order to write: the old order, then the new keys. */
250
+ export function completeOrder(
251
+ props: Record<string, PlutoPropValue>,
252
+ order: string[]
253
+ ): string[] {
254
+ const out = order.filter((key) => key in props)
255
+ for (const key of Object.keys(props)) {
256
+ if (!out.includes(key)) {
257
+ out.push(key)
258
+ }
259
+ }
260
+ return out
261
+ }
262
+
263
+ // YAML props
264
+
265
+ /** Reads the lines of a YAML block, without the `---` lines and without the indent. */
266
+ export function parseYamlProps(text: string): ParsedProps | undefined {
267
+ const document = parseDocument(text)
268
+ if (document.errors.length > 0) {
269
+ return undefined
270
+ }
271
+ const json = document.toJSON()
272
+ if (json === null) {
273
+ return { props: {}, order: [], source: {} }
274
+ }
275
+ if (typeof json !== 'object' || Array.isArray(json)) {
276
+ return undefined
277
+ }
278
+ const result: ParsedProps = { props: {}, order: [], source: {} }
279
+ const contents = document.contents as {
280
+ items?: Array<{
281
+ key: { value: unknown; range?: number[] | null }
282
+ value: { range?: number[] | null } | null
283
+ }>
284
+ } | null
285
+ for (const item of contents?.items ?? []) {
286
+ const key = String(item.key.value)
287
+ const start = item.key.range?.[0]
288
+ const end = item.value?.range?.[1] ?? item.key.range?.[1]
289
+ result.props[key] = (json as Record<string, unknown>)[key]
290
+ result.order.push(key)
291
+ if (start !== undefined && end !== undefined) {
292
+ result.source[key] = {
293
+ value: result.props[key],
294
+ text: text.slice(start, end).trimEnd(),
295
+ }
296
+ }
297
+ }
298
+ return result
299
+ }
300
+
301
+ /** Writes the YAML entries (no `---` lines), one text for each prop, without indent. */
302
+ export function renderYamlProps(
303
+ props: Record<string, PlutoPropValue>,
304
+ order: string[],
305
+ source: Record<string, PropSource>
306
+ ): string[] {
307
+ const entries: string[] = []
308
+ for (const key of order) {
309
+ const value = props[key]
310
+ if (value === undefined) {
311
+ continue
312
+ }
313
+ const known = source[key]
314
+ entries.push(
315
+ known && isEqualValue(known.value, value)
316
+ ? known.text
317
+ : stringify({ [key]: value }, { lineWidth: 0 }).trimEnd()
318
+ )
319
+ }
320
+ return entries
321
+ }