@nuasite/cms 0.41.0 → 0.42.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/dist/editor.js +10589 -10349
- package/package.json +1 -1
- package/src/collection-scanner.ts +169 -48
- package/src/content-config-ast.ts +81 -3
- package/src/editor/components/collections-browser.tsx +20 -4
- package/src/editor/components/frontmatter-fields.tsx +29 -1
- package/src/editor/components/markdown-editor-overlay.tsx +1 -1
- package/src/editor/components/markdown-inline-editor.tsx +50 -0
- package/src/editor/components/toolbar.tsx +17 -2
- package/src/editor/milkdown-utils.ts +9 -2
- package/src/editor/styled-list-plugin.ts +233 -0
- package/src/editor/types.ts +3 -0
- package/src/handlers/markdown-ops.ts +75 -1
- package/src/html-processor.ts +22 -7
- package/src/index.ts +9 -2
- package/src/rehype-cms-marker.ts +2 -2
- package/src/types.ts +6 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { remarkStringifyOptionsCtx } from '@milkdown/core'
|
|
2
|
+
import { bulletListAttr, bulletListSchema } from '@milkdown/preset-commonmark'
|
|
3
|
+
import type { Node as PmNode } from '@milkdown/prose/model'
|
|
4
|
+
import type { Command } from '@milkdown/prose/state'
|
|
5
|
+
import type { MarkdownNode, SerializerState } from '@milkdown/transformer'
|
|
6
|
+
import { $command, $remark } from '@milkdown/utils'
|
|
7
|
+
|
|
8
|
+
const LIST_DIRECTIVE_NAME = 'list'
|
|
9
|
+
|
|
10
|
+
function normalizeListStyleClass(value: unknown): string | null {
|
|
11
|
+
if (typeof value !== 'string') return null
|
|
12
|
+
const [className] = value.trim().split(/\s+/)
|
|
13
|
+
return className || null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getDirectiveClass(node: any): string | null {
|
|
17
|
+
const className = node.attributes?.class
|
|
18
|
+
return normalizeListStyleClass(className)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isParagraphText(node: any, value?: string): boolean {
|
|
22
|
+
if (node?.type !== 'paragraph' || node.children?.length !== 1) return false
|
|
23
|
+
const text = node.children[0]
|
|
24
|
+
if (text?.type !== 'text') return false
|
|
25
|
+
return value === undefined ? true : text.value === value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseOpeningDirective(node: any): string | null {
|
|
29
|
+
if (!isParagraphText(node)) return null
|
|
30
|
+
const value = node.children[0].value
|
|
31
|
+
const match = /^:::list\{\.([^}\s]+)\}$/.exec(value)
|
|
32
|
+
return match?.[1] ?? null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stripRawClosingDirective(listNode: any, siblings: any[], indexAfterList: number): boolean {
|
|
36
|
+
if (isParagraphText(siblings[indexAfterList], ':::')) {
|
|
37
|
+
siblings.splice(indexAfterList, 1)
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lastItem = listNode.children?.at(-1)
|
|
42
|
+
const lastBlock = lastItem?.children?.at(-1)
|
|
43
|
+
if (!lastBlock || lastBlock.type !== 'paragraph') return false
|
|
44
|
+
const lastText = lastBlock.children?.at(-1)
|
|
45
|
+
if (lastText?.type !== 'text' || typeof lastText.value !== 'string') return false
|
|
46
|
+
|
|
47
|
+
if (lastText.value === ':::') {
|
|
48
|
+
lastBlock.children.pop()
|
|
49
|
+
} else if (lastText.value.endsWith('\n:::')) {
|
|
50
|
+
lastText.value = lastText.value.slice(0, -4)
|
|
51
|
+
} else {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (lastBlock.children.length === 0) lastItem.children.pop()
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function transformListDirectives(parent: any): void {
|
|
60
|
+
if (!Array.isArray(parent?.children)) return
|
|
61
|
+
|
|
62
|
+
for (let index = 0; index < parent.children.length; index++) {
|
|
63
|
+
const child = parent.children[index]
|
|
64
|
+
|
|
65
|
+
if (child?.type === 'containerDirective' && child.name === LIST_DIRECTIVE_NAME) {
|
|
66
|
+
const className = getDirectiveClass(child)
|
|
67
|
+
const list = child.children?.length === 1 ? child.children[0] : null
|
|
68
|
+
if (className && list?.type === 'list' && !list.ordered) {
|
|
69
|
+
parent.children[index] = {
|
|
70
|
+
...list,
|
|
71
|
+
listStyle: className,
|
|
72
|
+
}
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rawClassName = parseOpeningDirective(child)
|
|
78
|
+
const rawList = parent.children[index + 1]
|
|
79
|
+
if (rawClassName && rawList?.type === 'list' && !rawList.ordered && stripRawClosingDirective(rawList, parent.children, index + 2)) {
|
|
80
|
+
parent.children.splice(index, 1)
|
|
81
|
+
parent.children[index] = {
|
|
82
|
+
...rawList,
|
|
83
|
+
listStyle: rawClassName,
|
|
84
|
+
}
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
transformListDirectives(child)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function listDirectiveHandler(node: any, _parent: any, state: any, info: any): string {
|
|
93
|
+
const className = normalizeListStyleClass(node.attributes?.class)
|
|
94
|
+
const children = state.containerFlow(node, info)
|
|
95
|
+
return `:::list${className ? `{.${className}}` : ''}\n${children}\n:::`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function remarkListDirective(this: { data: () => any }) {
|
|
99
|
+
const data = this.data()
|
|
100
|
+
const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = [])
|
|
101
|
+
toMarkdownExtensions.push({
|
|
102
|
+
handlers: {
|
|
103
|
+
containerDirective: listDirectiveHandler,
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return (tree: any) => {
|
|
108
|
+
transformListDirectives(tree)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const remarkListDirectivePlugin: any = $remark('remarkListDirective', () => remarkListDirective as any)
|
|
113
|
+
|
|
114
|
+
export const styledBulletListSchema = bulletListSchema.extendSchema((prev) => (ctx) => {
|
|
115
|
+
const schema = prev(ctx)
|
|
116
|
+
return {
|
|
117
|
+
...schema,
|
|
118
|
+
attrs: {
|
|
119
|
+
...schema.attrs,
|
|
120
|
+
listStyle: {
|
|
121
|
+
default: null,
|
|
122
|
+
validate: 'string|null',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
parseDOM: [
|
|
126
|
+
{
|
|
127
|
+
tag: 'ul',
|
|
128
|
+
getAttrs: (dom) => {
|
|
129
|
+
const previousAttrs = schema.parseDOM?.[0]?.getAttrs?.(dom) ?? {}
|
|
130
|
+
const className = dom instanceof HTMLElement ? normalizeListStyleClass(dom.className) : null
|
|
131
|
+
return {
|
|
132
|
+
...(typeof previousAttrs === 'object' ? previousAttrs : {}),
|
|
133
|
+
listStyle: className,
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
toDOM: (node: PmNode) => {
|
|
139
|
+
const className = normalizeListStyleClass(node.attrs.listStyle)
|
|
140
|
+
return [
|
|
141
|
+
'ul',
|
|
142
|
+
{
|
|
143
|
+
...ctx.get(bulletListAttr.key)(node),
|
|
144
|
+
...(className ? { class: className } : {}),
|
|
145
|
+
'data-spread': node.attrs.spread,
|
|
146
|
+
},
|
|
147
|
+
0,
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
parseMarkdown: {
|
|
151
|
+
match: ({ type, ordered }: MarkdownNode & { ordered?: boolean }) => type === 'list' && !ordered,
|
|
152
|
+
runner: (state, node: MarkdownNode & { spread?: boolean; listStyle?: string }, type) => {
|
|
153
|
+
state.openNode(type, {
|
|
154
|
+
spread: node.spread ?? false,
|
|
155
|
+
listStyle: normalizeListStyleClass(node.listStyle),
|
|
156
|
+
}).next(node.children).closeNode()
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
toMarkdown: {
|
|
160
|
+
match: (node: PmNode) => node.type.name === 'bullet_list',
|
|
161
|
+
runner: (state: SerializerState, node: PmNode) => {
|
|
162
|
+
const listStyle = normalizeListStyleClass(node.attrs.listStyle)
|
|
163
|
+
if (listStyle) {
|
|
164
|
+
state.openNode('containerDirective', undefined, {
|
|
165
|
+
name: LIST_DIRECTIVE_NAME,
|
|
166
|
+
attributes: { class: listStyle },
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
state
|
|
171
|
+
.openNode('list', undefined, {
|
|
172
|
+
ordered: false,
|
|
173
|
+
spread: node.attrs.spread === true,
|
|
174
|
+
})
|
|
175
|
+
.next(node.content)
|
|
176
|
+
.closeNode()
|
|
177
|
+
|
|
178
|
+
if (listStyle) state.closeNode()
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
export const setBulletListStyleCommand = $command('SetBulletListStyle', () => {
|
|
185
|
+
return (listStyle?: string | null): Command => {
|
|
186
|
+
return (state, dispatch) => {
|
|
187
|
+
const bulletListType = state.schema.nodes.bullet_list
|
|
188
|
+
if (!bulletListType) return false
|
|
189
|
+
|
|
190
|
+
const nextStyle = normalizeListStyleClass(listStyle)
|
|
191
|
+
const positions = new Map<number, PmNode>()
|
|
192
|
+
const { from, to, $from } = state.selection
|
|
193
|
+
|
|
194
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
195
|
+
const node = $from.node(depth)
|
|
196
|
+
if (node.type === bulletListType) {
|
|
197
|
+
positions.set($from.before(depth), node)
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
203
|
+
if (node.type === bulletListType) positions.set(pos, node)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
if (positions.size === 0) return false
|
|
207
|
+
|
|
208
|
+
if (dispatch) {
|
|
209
|
+
let tr = state.tr
|
|
210
|
+
for (const [pos, node] of positions) {
|
|
211
|
+
tr = tr.setNodeMarkup(pos, undefined, {
|
|
212
|
+
...node.attrs,
|
|
213
|
+
listStyle: nextStyle,
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
dispatch(tr)
|
|
217
|
+
}
|
|
218
|
+
return true
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
export const styledListPlugin = [
|
|
224
|
+
remarkListDirectivePlugin,
|
|
225
|
+
styledBulletListSchema,
|
|
226
|
+
setBulletListStyleCommand,
|
|
227
|
+
(ctx: any) => () => {
|
|
228
|
+
ctx.update(remarkStringifyOptionsCtx, (options: any) => ({
|
|
229
|
+
...options,
|
|
230
|
+
bullet: '-',
|
|
231
|
+
}))
|
|
232
|
+
},
|
|
233
|
+
].flat()
|
package/src/editor/types.ts
CHANGED
|
@@ -52,6 +52,9 @@ export interface CmsConfig {
|
|
|
52
52
|
theme?: CmsThemeConfig
|
|
53
53
|
themePreset?: CmsThemePreset
|
|
54
54
|
features?: CmsFeatures
|
|
55
|
+
listStyles?: Array<{ label: string; class: string }>
|
|
56
|
+
/** Open the entry editor's metadata (frontmatter) panel by default instead of collapsed. */
|
|
57
|
+
openMetadataByDefault?: boolean
|
|
55
58
|
/** Maximum upload size in bytes for media uploads (injected by the integration). */
|
|
56
59
|
maxUploadSize?: number
|
|
57
60
|
/**
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { Dirent } from 'node:fs'
|
|
1
2
|
import fs from 'node:fs/promises'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import yaml from 'yaml'
|
|
4
5
|
import { getProjectRoot } from '../config'
|
|
6
|
+
import { parseContentConfig } from '../content-config-ast'
|
|
5
7
|
import type { ComponentDefinition } from '../types'
|
|
6
8
|
import { acquireFileLock, isNodeError, relativeImportPath, resolveAndValidatePath, slugify } from '../utils'
|
|
7
9
|
|
|
@@ -149,7 +151,10 @@ export async function handleCreateMarkdown(
|
|
|
149
151
|
return { success: false, error: `Invalid file extension "${ext}". Allowed: ${allowedExtensions.join(', ')}` }
|
|
150
152
|
}
|
|
151
153
|
const isData = ext === 'json' || ext === 'yaml' || ext === 'yml'
|
|
152
|
-
const
|
|
154
|
+
const layout = isData ? 'flat' : await detectCollectionMarkdownLayout(collection)
|
|
155
|
+
const filePath = layout === 'index'
|
|
156
|
+
? `src/content/${collection}/${normalizedSlug}/index.${ext}`
|
|
157
|
+
: `src/content/${collection}/${normalizedSlug}.${ext}`
|
|
153
158
|
const fullPath = resolveAndValidatePath(filePath)
|
|
154
159
|
|
|
155
160
|
let fileContent: string
|
|
@@ -275,6 +280,75 @@ function isDataFile(filePath: string): boolean {
|
|
|
275
280
|
return filePath.endsWith('.json') || filePath.endsWith('.yaml') || filePath.endsWith('.yml')
|
|
276
281
|
}
|
|
277
282
|
|
|
283
|
+
type MarkdownCollectionLayout = 'flat' | 'index'
|
|
284
|
+
|
|
285
|
+
async function detectCollectionMarkdownLayout(collection: string): Promise<MarkdownCollectionLayout> {
|
|
286
|
+
const existingLayout = await inferLayoutFromExistingEntries(collection)
|
|
287
|
+
if (existingLayout) return existingLayout
|
|
288
|
+
|
|
289
|
+
const configLayout = await inferLayoutFromContentConfig(collection)
|
|
290
|
+
if (configLayout) return configLayout
|
|
291
|
+
|
|
292
|
+
return 'flat'
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function inferLayoutFromExistingEntries(collection: string): Promise<MarkdownCollectionLayout | null> {
|
|
296
|
+
const collectionPath = path.join(getProjectRoot(), 'src', 'content', collection)
|
|
297
|
+
|
|
298
|
+
let dirEntries: Dirent[]
|
|
299
|
+
try {
|
|
300
|
+
dirEntries = await fs.readdir(collectionPath, { withFileTypes: true })
|
|
301
|
+
} catch {
|
|
302
|
+
return null
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let flatCount = 0
|
|
306
|
+
let indexCount = 0
|
|
307
|
+
const flatSlugs = new Set<string>()
|
|
308
|
+
|
|
309
|
+
for (const entry of dirEntries) {
|
|
310
|
+
if (!entry.isFile()) continue
|
|
311
|
+
const match = entry.name.match(/^(.+)\.(md|mdx)$/)
|
|
312
|
+
if (!match) continue
|
|
313
|
+
flatCount++
|
|
314
|
+
flatSlugs.add(match[1]!)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const subdirs = dirEntries.filter(entry => entry.isDirectory() && !entry.name.startsWith('_') && !entry.name.startsWith('.'))
|
|
318
|
+
const indexLookups = await Promise.all(subdirs.map(async dir => {
|
|
319
|
+
if (flatSlugs.has(dir.name)) return false
|
|
320
|
+
for (const ext of ['md', 'mdx'] as const) {
|
|
321
|
+
try {
|
|
322
|
+
await fs.access(path.join(collectionPath, dir.name, `index.${ext}`))
|
|
323
|
+
return true
|
|
324
|
+
} catch {
|
|
325
|
+
// try next extension
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return false
|
|
329
|
+
}))
|
|
330
|
+
indexCount = indexLookups.filter(Boolean).length
|
|
331
|
+
|
|
332
|
+
if (indexCount > flatCount) return 'index'
|
|
333
|
+
if (flatCount > 0) return 'flat'
|
|
334
|
+
return null
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function inferLayoutFromContentConfig(collection: string): Promise<MarkdownCollectionLayout | null> {
|
|
338
|
+
try {
|
|
339
|
+
const parsed = await parseContentConfig()
|
|
340
|
+
const pattern = parsed.get(collection)?.loaderPattern
|
|
341
|
+
if (!pattern) return null
|
|
342
|
+
return isIndexStyleGlobPattern(pattern) ? 'index' : 'flat'
|
|
343
|
+
} catch {
|
|
344
|
+
return null
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isIndexStyleGlobPattern(pattern: string): boolean {
|
|
349
|
+
return pattern.includes('index.{') || pattern.includes('*/index') || pattern.includes('**/index')
|
|
350
|
+
}
|
|
351
|
+
|
|
278
352
|
function parseFrontmatter(raw: string): { frontmatter: Record<string, unknown>; content: string } {
|
|
279
353
|
const trimmed = raw.trimStart()
|
|
280
354
|
if (!trimmed.startsWith('---')) {
|
package/src/html-processor.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type HTMLElement as ParsedHTMLElement, parse } from 'node-html-parser'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getProjectRoot } from './config'
|
|
2
4
|
import { processSeoFromHtml } from './seo-processor'
|
|
3
5
|
|
|
4
6
|
import { extractBackgroundImageClasses, extractColorClasses, extractTextStyleClasses } from './tailwind-colors'
|
|
@@ -283,6 +285,7 @@ export async function processHtml(
|
|
|
283
285
|
const components: Record<string, ComponentInstance> = {}
|
|
284
286
|
const sourceLocationMap = new Map<string, { file: string; line: number }>()
|
|
285
287
|
const markedComponentRoots = new Set<HTMLNode>()
|
|
288
|
+
const markdownRegionPaths = new Map<string, string>()
|
|
286
289
|
let collectionWrapperId: string | undefined
|
|
287
290
|
const componentCountPerParent = new Map<string, Map<string, number>>()
|
|
288
291
|
|
|
@@ -462,6 +465,7 @@ export async function processHtml(
|
|
|
462
465
|
// Collection wrapper detection pass: find the element that wraps markdown content
|
|
463
466
|
// This needs to run BEFORE image marking so we can skip images inside markdown
|
|
464
467
|
let markdownWrapperNode: HTMLNode | null = null
|
|
468
|
+
const markdownWrapperNodes = new Set<HTMLNode>()
|
|
465
469
|
|
|
466
470
|
// Three strategies in priority order:
|
|
467
471
|
// 0. Rehype marker: the rehype-cms-marker plugin marks the first rendered element
|
|
@@ -473,16 +477,24 @@ export async function processHtml(
|
|
|
473
477
|
let foundWrapper = false
|
|
474
478
|
|
|
475
479
|
// Strategy 0: Rehype marker — most reliable
|
|
476
|
-
const
|
|
477
|
-
|
|
480
|
+
const markerEls = root.querySelectorAll('[data-cms-markdown-content]')
|
|
481
|
+
for (const markerEl of markerEls) {
|
|
482
|
+
const markerPath = markerEl.getAttribute('data-cms-markdown-content') ?? ''
|
|
478
483
|
markerEl.removeAttribute('data-cms-markdown-content')
|
|
479
484
|
const parent = markerEl.parentNode as HTMLNode | null
|
|
480
485
|
if (parent && parent.tagName) {
|
|
481
486
|
const id = getNextId()
|
|
482
487
|
parent.setAttribute(attributeName, id)
|
|
483
488
|
parent.setAttribute('data-cms-markdown', 'true')
|
|
484
|
-
|
|
485
|
-
|
|
489
|
+
if (markerPath) {
|
|
490
|
+
const relPath = path.relative(getProjectRoot(), markerPath).split(path.sep).join('/').replace(/\\/g, '/')
|
|
491
|
+
markdownRegionPaths.set(id, relPath)
|
|
492
|
+
}
|
|
493
|
+
if (!collectionWrapperId) {
|
|
494
|
+
collectionWrapperId = id
|
|
495
|
+
markdownWrapperNode = parent
|
|
496
|
+
}
|
|
497
|
+
markdownWrapperNodes.add(parent)
|
|
486
498
|
foundWrapper = true
|
|
487
499
|
}
|
|
488
500
|
}
|
|
@@ -517,6 +529,7 @@ export async function processHtml(
|
|
|
517
529
|
node.setAttribute('data-cms-markdown', 'true')
|
|
518
530
|
collectionWrapperId = id
|
|
519
531
|
markdownWrapperNode = node
|
|
532
|
+
markdownWrapperNodes.add(node)
|
|
520
533
|
foundWrapper = true
|
|
521
534
|
}
|
|
522
535
|
}
|
|
@@ -584,6 +597,7 @@ export async function processHtml(
|
|
|
584
597
|
bestWrapper.setAttribute('data-cms-markdown', 'true')
|
|
585
598
|
collectionWrapperId = id
|
|
586
599
|
markdownWrapperNode = bestWrapper
|
|
600
|
+
markdownWrapperNodes.add(bestWrapper)
|
|
587
601
|
foundWrapper = true
|
|
588
602
|
}
|
|
589
603
|
}
|
|
@@ -635,6 +649,7 @@ export async function processHtml(
|
|
|
635
649
|
best.node.setAttribute('data-cms-markdown', 'true')
|
|
636
650
|
collectionWrapperId = id
|
|
637
651
|
markdownWrapperNode = best.node
|
|
652
|
+
markdownWrapperNodes.add(best.node)
|
|
638
653
|
foundWrapper = true
|
|
639
654
|
}
|
|
640
655
|
}
|
|
@@ -644,10 +659,10 @@ export async function processHtml(
|
|
|
644
659
|
|
|
645
660
|
// Helper function to check if a node is inside the markdown wrapper
|
|
646
661
|
const isInsideMarkdownWrapper = (node: HTMLNode): boolean => {
|
|
647
|
-
if (!markdownWrapperNode) return false
|
|
662
|
+
if (!markdownWrapperNode && markdownWrapperNodes.size === 0) return false
|
|
648
663
|
let current = node.parentNode as HTMLNode | null
|
|
649
664
|
while (current) {
|
|
650
|
-
if (current === markdownWrapperNode) return true
|
|
665
|
+
if (current === markdownWrapperNode || markdownWrapperNodes.has(current)) return true
|
|
651
666
|
current = current.parentNode as HTMLNode | null
|
|
652
667
|
}
|
|
653
668
|
return false
|
|
@@ -998,7 +1013,7 @@ export async function processHtml(
|
|
|
998
1013
|
// Add collection info for the wrapper entry
|
|
999
1014
|
collectionName: isCollectionWrapper ? collectionInfo?.name : undefined,
|
|
1000
1015
|
collectionSlug: isCollectionWrapper ? collectionInfo?.slug : undefined,
|
|
1001
|
-
contentPath: isCollectionWrapper ? collectionInfo?.contentPath : undefined,
|
|
1016
|
+
contentPath: markdownRegionPaths.get(id) ?? (isCollectionWrapper ? collectionInfo?.contentPath : undefined),
|
|
1002
1017
|
// Robustness fields
|
|
1003
1018
|
stableId,
|
|
1004
1019
|
// Image metadata for image entries
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,9 @@ export interface NuaCmsOptions extends CmsMarkerOptions {
|
|
|
34
34
|
theme?: Record<string, string>
|
|
35
35
|
themePreset?: string
|
|
36
36
|
features?: CmsFeatures
|
|
37
|
+
listStyles?: Array<{ label: string; class: string }>
|
|
38
|
+
/** Open the entry editor's metadata (frontmatter) panel by default instead of collapsed. */
|
|
39
|
+
openMetadataByDefault?: boolean
|
|
37
40
|
/**
|
|
38
41
|
* Describes the host site's color theme. The CMS draws editor chrome and outlines
|
|
39
42
|
* in a contrasting color. 'auto' (default) detects via prefers-color-scheme and
|
|
@@ -58,10 +61,12 @@ export interface NuaCmsOptions extends CmsMarkerOptions {
|
|
|
58
61
|
*/
|
|
59
62
|
mdxComponentDirs?: string[]
|
|
60
63
|
/**
|
|
61
|
-
* Per-collection
|
|
64
|
+
* Per-collection overrides for the CMS browser.
|
|
62
65
|
* Highest priority — overrides scanner defaults and frontmatter comment directives.
|
|
63
66
|
*/
|
|
64
67
|
collections?: Record<string, {
|
|
68
|
+
/** Display label shown in the CMS (overrides the name-derived default, e.g. "Jsem Otazky" → "Otázky"). */
|
|
69
|
+
label?: string
|
|
65
70
|
fields?: Record<string, { position?: 'sidebar' | 'header'; group?: string }>
|
|
66
71
|
}>
|
|
67
72
|
/**
|
|
@@ -185,7 +190,9 @@ export default function nuaCms(options: NuaCmsOptions = {}): AstroIntegration {
|
|
|
185
190
|
if (options.collections) {
|
|
186
191
|
for (const [collectionName, overrides] of Object.entries(options.collections)) {
|
|
187
192
|
const def = collectionDefinitions[collectionName]
|
|
188
|
-
if (!def
|
|
193
|
+
if (!def) continue
|
|
194
|
+
if (overrides.label) def.label = overrides.label
|
|
195
|
+
if (!overrides.fields) continue
|
|
189
196
|
for (const field of def.fields) {
|
|
190
197
|
const fieldOverride = overrides.fields[field.name]
|
|
191
198
|
if (!fieldOverride) continue
|
package/src/rehype-cms-marker.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* relying on heuristics.
|
|
6
6
|
*/
|
|
7
7
|
export function rehypeCmsMarker() {
|
|
8
|
-
return (tree: any) => {
|
|
8
|
+
return (tree: any, file: any) => {
|
|
9
9
|
const firstElement = tree.children?.find((n: any) => n.type === 'element')
|
|
10
10
|
if (firstElement) {
|
|
11
11
|
firstElement.properties ??= {}
|
|
12
|
-
firstElement.properties['dataCmsMarkdownContent'] = ''
|
|
12
|
+
firstElement.properties['dataCmsMarkdownContent'] = file?.path ?? (Array.isArray(file?.history) ? file.history[0] : '') ?? ''
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
}
|
package/src/types.ts
CHANGED
|
@@ -345,6 +345,12 @@ export interface CollectionDefinition {
|
|
|
345
345
|
orderBy?: string
|
|
346
346
|
/** Sort direction for orderBy field */
|
|
347
347
|
orderDirection?: 'asc' | 'desc'
|
|
348
|
+
/**
|
|
349
|
+
* Name of the collection this one is nested under in the CMS browser, when it shares a base
|
|
350
|
+
* directory with another collection (e.g. a nested `*/otazky/*` collection grouped under the
|
|
351
|
+
* `*/index.md` collection at the same base). Purely presentational grouping.
|
|
352
|
+
*/
|
|
353
|
+
parentCollection?: string
|
|
348
354
|
}
|
|
349
355
|
|
|
350
356
|
/** Manifest metadata for versioning and conflict detection */
|