@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,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('"', '"').replaceAll(''', "'"),
|
|
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('"', '"')}"`
|
|
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
|
+
}
|