@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,94 @@
1
+ // Undo and redo for the block doc. The history keeps snapshots of the doc.
2
+ // A snapshot is immutable, because every operation returns a new doc. The
3
+ // clock is a parameter, so a test needs no timers.
4
+
5
+ export const HISTORY_LIMIT = 100
6
+
7
+ /** Edits of the same key within this time (ms) join in one step. */
8
+ export const HISTORY_MERGE_MS = 600
9
+
10
+ export interface PlutoHistory<T> {
11
+ /** Oldest first. */
12
+ past: T[]
13
+ present: T
14
+ future: T[]
15
+ /** The key and the time of the last push, to join typing in one block. */
16
+ last?: { key: string; at: number }
17
+ }
18
+
19
+ export interface PlutoHistoryOptions {
20
+ limit?: number
21
+ mergeMs?: number
22
+ }
23
+
24
+ export function createHistory<T>(initial: T): PlutoHistory<T> {
25
+ return { past: [], present: initial, future: [] }
26
+ }
27
+
28
+ export function canUndo(history: PlutoHistory<unknown>): boolean {
29
+ return history.past.length > 0
30
+ }
31
+
32
+ export function canRedo(history: PlutoHistory<unknown>): boolean {
33
+ return history.future.length > 0
34
+ }
35
+
36
+ /**
37
+ * Adds a new state. A state equal (`===`) to the present is ignored. With a
38
+ * `key` (for example the id of a block), a push with the same key within
39
+ * `mergeMs` of the last push replaces the present, so it is one undo step.
40
+ */
41
+ export function pushHistory<T>(
42
+ history: PlutoHistory<T>,
43
+ state: T,
44
+ now: number,
45
+ key?: string,
46
+ options: PlutoHistoryOptions = {}
47
+ ): PlutoHistory<T> {
48
+ if (state === history.present) {
49
+ return history
50
+ }
51
+ const limit = options.limit ?? HISTORY_LIMIT
52
+ const mergeMs = options.mergeMs ?? HISTORY_MERGE_MS
53
+ const merge =
54
+ key !== undefined &&
55
+ history.last?.key === key &&
56
+ now - history.last.at <= mergeMs &&
57
+ history.past.length > 0 &&
58
+ history.future.length === 0
59
+ const last = key === undefined ? undefined : { key, at: now }
60
+ if (merge) {
61
+ return { ...history, present: state, last }
62
+ }
63
+ return {
64
+ past: [...history.past, history.present].slice(-limit),
65
+ present: state,
66
+ future: [],
67
+ last,
68
+ }
69
+ }
70
+
71
+ export function undo<T>(history: PlutoHistory<T>): PlutoHistory<T> {
72
+ if (!canUndo(history)) {
73
+ return history
74
+ }
75
+ const past = history.past.slice(0, -1)
76
+ return {
77
+ past,
78
+ present: history.past.at(-1)!,
79
+ future: [history.present, ...history.future],
80
+ }
81
+ }
82
+
83
+ export function redo<T>(history: PlutoHistory<T>): PlutoHistory<T> {
84
+ if (!canRedo(history)) {
85
+ return history
86
+ }
87
+ const [present, ...future] = history.future
88
+ return { past: [...history.past, history.present], present: present!, future }
89
+ }
90
+
91
+ /** Starts again from a state, for example after a switch to the Markdown mode. */
92
+ export function resetHistory<T>(state: T): PlutoHistory<T> {
93
+ return createHistory(state)
94
+ }
@@ -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
+ }