@nuasite/cms 0.1.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/README.md +237 -0
- package/dist/src/build-processor.d.ts +20 -0
- package/dist/src/build-processor.d.ts.map +1 -0
- package/dist/src/collection-scanner.d.ts +6 -0
- package/dist/src/collection-scanner.d.ts.map +1 -0
- package/dist/src/component-registry.d.ts +63 -0
- package/dist/src/component-registry.d.ts.map +1 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/dev-middleware.d.ts +20 -0
- package/dist/src/dev-middleware.d.ts.map +1 -0
- package/dist/src/editor/ai.d.ts +60 -0
- package/dist/src/editor/ai.d.ts.map +1 -0
- package/dist/src/editor/api.d.ts +140 -0
- package/dist/src/editor/api.d.ts.map +1 -0
- package/dist/src/editor/color-utils.d.ts +106 -0
- package/dist/src/editor/color-utils.d.ts.map +1 -0
- package/dist/src/editor/components/ai-chat.d.ts +11 -0
- package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
- package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
- package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
- package/dist/src/editor/components/attribute-editor.d.ts +5 -0
- package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
- package/dist/src/editor/components/block-editor.d.ts +12 -0
- package/dist/src/editor/components/block-editor.d.ts.map +1 -0
- package/dist/src/editor/components/collections-browser.d.ts +2 -0
- package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
- package/dist/src/editor/components/color-toolbar.d.ts +12 -0
- package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
- package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
- package/dist/src/editor/components/create-page-modal.d.ts +2 -0
- package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
- package/dist/src/editor/components/editable-highlights.d.ts +9 -0
- package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
- package/dist/src/editor/components/error-boundary.d.ts +32 -0
- package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
- package/dist/src/editor/components/fields.d.ts +75 -0
- package/dist/src/editor/components/fields.d.ts.map +1 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
- package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
- package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/image-overlay.d.ts +12 -0
- package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
- package/dist/src/editor/components/media-library.d.ts +2 -0
- package/dist/src/editor/components/media-library.d.ts.map +1 -0
- package/dist/src/editor/components/outline.d.ts +21 -0
- package/dist/src/editor/components/outline.d.ts.map +1 -0
- package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
- package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
- package/dist/src/editor/components/seo-editor.d.ts +2 -0
- package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
- package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast.d.ts +7 -0
- package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
- package/dist/src/editor/components/toast/types.d.ts +7 -0
- package/dist/src/editor/components/toast/types.d.ts.map +1 -0
- package/dist/src/editor/components/toolbar.d.ts +21 -0
- package/dist/src/editor/components/toolbar.d.ts.map +1 -0
- package/dist/src/editor/config.d.ts +4 -0
- package/dist/src/editor/config.d.ts.map +1 -0
- package/dist/src/editor/constants.d.ts +101 -0
- package/dist/src/editor/constants.d.ts.map +1 -0
- package/dist/src/editor/context.d.ts +14 -0
- package/dist/src/editor/context.d.ts.map +1 -0
- package/dist/src/editor/dom.d.ts +77 -0
- package/dist/src/editor/dom.d.ts.map +1 -0
- package/dist/src/editor/editor.d.ts +64 -0
- package/dist/src/editor/editor.d.ts.map +1 -0
- package/dist/src/editor/history.d.ts +20 -0
- package/dist/src/editor/history.d.ts.map +1 -0
- package/dist/src/editor/hooks/index.d.ts +14 -0
- package/dist/src/editor/hooks/index.d.ts.map +1 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
- package/dist/src/editor/hooks/utils.d.ts +32 -0
- package/dist/src/editor/hooks/utils.d.ts.map +1 -0
- package/dist/src/editor/index.d.ts +12 -0
- package/dist/src/editor/index.d.ts.map +1 -0
- package/dist/src/editor/lib/cn.d.ts +3 -0
- package/dist/src/editor/lib/cn.d.ts.map +1 -0
- package/dist/src/editor/manifest.d.ts +19 -0
- package/dist/src/editor/manifest.d.ts.map +1 -0
- package/dist/src/editor/markdown-api.d.ts +36 -0
- package/dist/src/editor/markdown-api.d.ts.map +1 -0
- package/dist/src/editor/signals.d.ts +242 -0
- package/dist/src/editor/signals.d.ts.map +1 -0
- package/dist/src/editor/storage.d.ts +27 -0
- package/dist/src/editor/storage.d.ts.map +1 -0
- package/dist/src/editor/text-styling.d.ts +350 -0
- package/dist/src/editor/text-styling.d.ts.map +1 -0
- package/dist/src/editor/themes.d.ts +38 -0
- package/dist/src/editor/themes.d.ts.map +1 -0
- package/dist/src/editor/types.d.ts +454 -0
- package/dist/src/editor/types.d.ts.map +1 -0
- package/dist/src/error-collector.d.ts +56 -0
- package/dist/src/error-collector.d.ts.map +1 -0
- package/dist/src/handlers/component-ops.d.ts +34 -0
- package/dist/src/handlers/component-ops.d.ts.map +1 -0
- package/dist/src/handlers/markdown-ops.d.ts +41 -0
- package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
- package/dist/src/handlers/request-utils.d.ts +20 -0
- package/dist/src/handlers/request-utils.d.ts.map +1 -0
- package/dist/src/handlers/source-writer.d.ts +51 -0
- package/dist/src/handlers/source-writer.d.ts.map +1 -0
- package/dist/src/html-processor.d.ts +63 -0
- package/dist/src/html-processor.d.ts.map +1 -0
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/manifest-writer.d.ts +111 -0
- package/dist/src/manifest-writer.d.ts.map +1 -0
- package/dist/src/media/contember.d.ts +15 -0
- package/dist/src/media/contember.d.ts.map +1 -0
- package/dist/src/media/local.d.ts +9 -0
- package/dist/src/media/local.d.ts.map +1 -0
- package/dist/src/media/s3.d.ts +12 -0
- package/dist/src/media/s3.d.ts.map +1 -0
- package/dist/src/media/types.d.ts +40 -0
- package/dist/src/media/types.d.ts.map +1 -0
- package/dist/src/preview-generator.d.ts +19 -0
- package/dist/src/preview-generator.d.ts.map +1 -0
- package/dist/src/seo-processor.d.ts +23 -0
- package/dist/src/seo-processor.d.ts.map +1 -0
- package/dist/src/source-finder/ast-extractors.d.ts +35 -0
- package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
- package/dist/src/source-finder/ast-parser.d.ts +16 -0
- package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
- package/dist/src/source-finder/cache.d.ts +18 -0
- package/dist/src/source-finder/cache.d.ts.map +1 -0
- package/dist/src/source-finder/collection-finder.d.ts +29 -0
- package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
- package/dist/src/source-finder/element-finder.d.ts +42 -0
- package/dist/src/source-finder/element-finder.d.ts.map +1 -0
- package/dist/src/source-finder/image-finder.d.ts +24 -0
- package/dist/src/source-finder/image-finder.d.ts.map +1 -0
- package/dist/src/source-finder/index.d.ts +9 -0
- package/dist/src/source-finder/index.d.ts.map +1 -0
- package/dist/src/source-finder/search-index.d.ts +27 -0
- package/dist/src/source-finder/search-index.d.ts.map +1 -0
- package/dist/src/source-finder/snippet-utils.d.ts +90 -0
- package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
- package/dist/src/source-finder/source-lookup.d.ts +16 -0
- package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
- package/dist/src/source-finder/types.d.ts +167 -0
- package/dist/src/source-finder/types.d.ts.map +1 -0
- package/dist/src/source-finder/variable-extraction.d.ts +37 -0
- package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
- package/dist/src/tailwind-colors.d.ts +54 -0
- package/dist/src/tailwind-colors.d.ts.map +1 -0
- package/dist/src/tsconfig.tsbuildinfo +1 -0
- package/dist/src/types.d.ts +367 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +61 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/vite-plugin.d.ts +14 -0
- package/dist/src/vite-plugin.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/build-processor.ts +784 -0
- package/src/collection-scanner.ts +304 -0
- package/src/component-registry.ts +393 -0
- package/src/config.ts +74 -0
- package/src/dev-middleware.ts +525 -0
- package/src/dist/src/tsconfig.tsbuildinfo +1 -0
- package/src/editor/ai.ts +185 -0
- package/src/editor/api.ts +513 -0
- package/src/editor/color-utils.ts +556 -0
- package/src/editor/components/ai-chat.tsx +632 -0
- package/src/editor/components/ai-tooltip.tsx +179 -0
- package/src/editor/components/attribute-editor.tsx +596 -0
- package/src/editor/components/block-editor.tsx +546 -0
- package/src/editor/components/collections-browser.tsx +248 -0
- package/src/editor/components/color-toolbar.tsx +314 -0
- package/src/editor/components/confirm-dialog.tsx +69 -0
- package/src/editor/components/create-page-modal.tsx +163 -0
- package/src/editor/components/editable-highlights.tsx +260 -0
- package/src/editor/components/error-boundary.tsx +87 -0
- package/src/editor/components/fields.tsx +387 -0
- package/src/editor/components/frontmatter-fields.tsx +469 -0
- package/src/editor/components/highlight-overlay.ts +229 -0
- package/src/editor/components/image-overlay.tsx +230 -0
- package/src/editor/components/markdown-editor-overlay.tsx +505 -0
- package/src/editor/components/markdown-inline-editor.tsx +780 -0
- package/src/editor/components/media-library.tsx +297 -0
- package/src/editor/components/outline.tsx +402 -0
- package/src/editor/components/redirect-countdown.tsx +45 -0
- package/src/editor/components/seo-editor.tsx +498 -0
- package/src/editor/components/text-style-toolbar.tsx +362 -0
- package/src/editor/components/toast/toast-container.tsx +15 -0
- package/src/editor/components/toast/toast.tsx +49 -0
- package/src/editor/components/toast/types.ts +7 -0
- package/src/editor/components/toolbar.tsx +366 -0
- package/src/editor/config.ts +12 -0
- package/src/editor/constants.ts +106 -0
- package/src/editor/context.tsx +38 -0
- package/src/editor/dom.ts +357 -0
- package/src/editor/editor.ts +1510 -0
- package/src/editor/env.d.ts +4 -0
- package/src/editor/history.ts +355 -0
- package/src/editor/hooks/index.ts +19 -0
- package/src/editor/hooks/useAIHandlers.ts +345 -0
- package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
- package/src/editor/hooks/useElementDetection.ts +284 -0
- package/src/editor/hooks/useImageHoverDetection.ts +102 -0
- package/src/editor/hooks/useTextSelection.ts +187 -0
- package/src/editor/hooks/useTooltipState.ts +126 -0
- package/src/editor/hooks/utils.ts +101 -0
- package/src/editor/index.tsx +481 -0
- package/src/editor/lib/cn.ts +4 -0
- package/src/editor/manifest.ts +25 -0
- package/src/editor/markdown-api.ts +209 -0
- package/src/editor/signals.ts +1351 -0
- package/src/editor/storage.ts +266 -0
- package/src/editor/styles.css +465 -0
- package/src/editor/text-styling.ts +773 -0
- package/src/editor/themes.ts +210 -0
- package/src/editor/types.ts +591 -0
- package/src/error-collector.ts +106 -0
- package/src/handlers/component-ops.ts +463 -0
- package/src/handlers/markdown-ops.ts +202 -0
- package/src/handlers/request-utils.ts +151 -0
- package/src/handlers/source-writer.ts +649 -0
- package/src/html-processor.ts +1108 -0
- package/src/index.ts +284 -0
- package/src/manifest-writer.ts +371 -0
- package/src/media/contember.ts +84 -0
- package/src/media/local.ts +114 -0
- package/src/media/s3.ts +133 -0
- package/src/media/types.ts +33 -0
- package/src/preview-generator.ts +293 -0
- package/src/seo-processor.ts +567 -0
- package/src/source-finder/ast-extractors.ts +185 -0
- package/src/source-finder/ast-parser.ts +150 -0
- package/src/source-finder/cache.ts +76 -0
- package/src/source-finder/collection-finder.ts +335 -0
- package/src/source-finder/cross-file-tracker.ts +741 -0
- package/src/source-finder/element-finder.ts +387 -0
- package/src/source-finder/image-finder.ts +283 -0
- package/src/source-finder/index.ts +37 -0
- package/src/source-finder/search-index.ts +525 -0
- package/src/source-finder/snippet-utils.ts +668 -0
- package/src/source-finder/source-lookup.ts +200 -0
- package/src/source-finder/types.ts +210 -0
- package/src/source-finder/variable-extraction.ts +406 -0
- package/src/tailwind-colors.ts +874 -0
- package/src/tsconfig.json +25 -0
- package/src/types.ts +406 -0
- package/src/utils.ts +186 -0
- package/src/vite-plugin.ts +42 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getProjectRoot } from '../config'
|
|
4
|
+
import type { ManifestWriter } from '../manifest-writer'
|
|
5
|
+
import type { CmsManifest, ComponentDefinition, ComponentInstance } from '../types'
|
|
6
|
+
import { acquireFileLock, escapeRegex, normalizePagePath, resolveAndValidatePath } from '../utils'
|
|
7
|
+
|
|
8
|
+
export type InsertPosition = 'before' | 'after'
|
|
9
|
+
|
|
10
|
+
export interface InsertComponentRequest {
|
|
11
|
+
position: InsertPosition
|
|
12
|
+
referenceComponentId: string
|
|
13
|
+
componentName: string
|
|
14
|
+
props: Record<string, unknown>
|
|
15
|
+
meta?: { source: string; url: string }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InsertComponentResponse {
|
|
19
|
+
success: boolean
|
|
20
|
+
message?: string
|
|
21
|
+
sourceFile?: string
|
|
22
|
+
error?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RemoveComponentRequest {
|
|
26
|
+
componentId: string
|
|
27
|
+
meta?: { source: string; url: string }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RemoveComponentResponse {
|
|
31
|
+
success: boolean
|
|
32
|
+
message?: string
|
|
33
|
+
sourceFile?: string
|
|
34
|
+
error?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function handleInsertComponent(
|
|
38
|
+
request: InsertComponentRequest,
|
|
39
|
+
manifestWriter: ManifestWriter,
|
|
40
|
+
): Promise<InsertComponentResponse> {
|
|
41
|
+
const { position, referenceComponentId, componentName, props, meta } = request
|
|
42
|
+
|
|
43
|
+
if (!meta?.url) {
|
|
44
|
+
return { success: false, error: 'Page URL is required in meta' }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const pagePath = normalizePagePath(meta.url)
|
|
48
|
+
const pageData = manifestWriter.getPageManifest(pagePath)
|
|
49
|
+
if (!pageData) {
|
|
50
|
+
return { success: false, error: 'Page manifest not found' }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const manifest: CmsManifest = {
|
|
54
|
+
entries: pageData.entries,
|
|
55
|
+
components: pageData.components,
|
|
56
|
+
componentDefinitions: manifestWriter.getComponentDefinitions(),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Find the reference component
|
|
60
|
+
const referenceComponent = manifest.components[referenceComponentId]
|
|
61
|
+
if (!referenceComponent) {
|
|
62
|
+
return { success: false, error: `Reference component '${referenceComponentId}' not found in manifest` }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get component definition
|
|
66
|
+
const componentDef = manifest.componentDefinitions[componentName]
|
|
67
|
+
if (!componentDef) {
|
|
68
|
+
return { success: false, error: `Component definition '${componentName}' not found in manifest` }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const projectRoot = getProjectRoot()
|
|
73
|
+
|
|
74
|
+
// Find the invocation file
|
|
75
|
+
const invocation = await findComponentInvocationFile(
|
|
76
|
+
projectRoot,
|
|
77
|
+
meta.url,
|
|
78
|
+
manifest,
|
|
79
|
+
referenceComponent,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const filePath = invocation?.filePath
|
|
83
|
+
?? normalizeFilePath(referenceComponent.invocationSourcePath ?? referenceComponent.sourcePath)
|
|
84
|
+
|
|
85
|
+
const fullPath = resolveAndValidatePath(filePath)
|
|
86
|
+
const release = await acquireFileLock(fullPath)
|
|
87
|
+
try {
|
|
88
|
+
let currentContent: string
|
|
89
|
+
try {
|
|
90
|
+
currentContent = await fs.readFile(fullPath, 'utf-8')
|
|
91
|
+
} catch {
|
|
92
|
+
return { success: false, error: `Source file not found: ${filePath}` }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const lines = currentContent.split('\n')
|
|
96
|
+
|
|
97
|
+
let refLineIndex: number
|
|
98
|
+
if (invocation) {
|
|
99
|
+
refLineIndex = invocation.lineIndex
|
|
100
|
+
} else {
|
|
101
|
+
const occurrenceIndex = getComponentOccurrenceIndex(manifest, referenceComponent)
|
|
102
|
+
refLineIndex = findComponentInvocationLine(lines, referenceComponent.componentName, occurrenceIndex)
|
|
103
|
+
if (refLineIndex < 0) {
|
|
104
|
+
refLineIndex = referenceComponent.sourceLine - 1
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (refLineIndex < 0 || refLineIndex >= lines.length) {
|
|
109
|
+
return { success: false, error: `Invalid source line for reference component: ${refLineIndex + 1}` }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const newComponentJsx = generateComponentJsx(componentName, props, componentDef)
|
|
113
|
+
|
|
114
|
+
const { startLine, endLine } = findComponentBounds(
|
|
115
|
+
lines,
|
|
116
|
+
refLineIndex,
|
|
117
|
+
referenceComponent.componentName,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const insertIndex = position === 'before' ? startLine : endLine + 1
|
|
121
|
+
const indentation = getIndentation(lines[startLine]!)
|
|
122
|
+
|
|
123
|
+
const indentedJsx = newComponentJsx
|
|
124
|
+
.split('\n')
|
|
125
|
+
.map((line) => (line.trim() ? indentation + line : line))
|
|
126
|
+
.join('\n')
|
|
127
|
+
|
|
128
|
+
lines.splice(insertIndex, 0, indentedJsx)
|
|
129
|
+
await fs.writeFile(fullPath, lines.join('\n'), 'utf-8')
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
message: `Successfully inserted ${componentName} ${position} ${referenceComponent.componentName}`,
|
|
134
|
+
sourceFile: filePath,
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
release()
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
141
|
+
return { success: false, error: message }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function handleRemoveComponent(
|
|
146
|
+
request: RemoveComponentRequest,
|
|
147
|
+
manifestWriter: ManifestWriter,
|
|
148
|
+
): Promise<RemoveComponentResponse> {
|
|
149
|
+
const { componentId, meta } = request
|
|
150
|
+
|
|
151
|
+
if (!meta?.url) {
|
|
152
|
+
return { success: false, error: 'Page URL is required in meta' }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const pagePath = normalizePagePath(meta.url)
|
|
156
|
+
const pageData = manifestWriter.getPageManifest(pagePath)
|
|
157
|
+
if (!pageData) {
|
|
158
|
+
return { success: false, error: 'Page manifest not found' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const manifest: CmsManifest = {
|
|
162
|
+
entries: pageData.entries,
|
|
163
|
+
components: pageData.components,
|
|
164
|
+
componentDefinitions: manifestWriter.getComponentDefinitions(),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const component = manifest.components[componentId]
|
|
168
|
+
if (!component) {
|
|
169
|
+
return { success: false, error: `Component '${componentId}' not found in manifest` }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const projectRoot = getProjectRoot()
|
|
174
|
+
|
|
175
|
+
const invocation = await findComponentInvocationFile(
|
|
176
|
+
projectRoot,
|
|
177
|
+
meta.url,
|
|
178
|
+
manifest,
|
|
179
|
+
component,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const filePath = invocation?.filePath
|
|
183
|
+
?? normalizeFilePath(component.invocationSourcePath ?? component.sourcePath)
|
|
184
|
+
|
|
185
|
+
const fullPath = resolveAndValidatePath(filePath)
|
|
186
|
+
const release = await acquireFileLock(fullPath)
|
|
187
|
+
try {
|
|
188
|
+
let currentContent: string
|
|
189
|
+
try {
|
|
190
|
+
currentContent = await fs.readFile(fullPath, 'utf-8')
|
|
191
|
+
} catch {
|
|
192
|
+
return { success: false, error: `Source file not found: ${filePath}` }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const lines = currentContent.split('\n')
|
|
196
|
+
|
|
197
|
+
let refLineIndex: number
|
|
198
|
+
if (invocation) {
|
|
199
|
+
refLineIndex = invocation.lineIndex
|
|
200
|
+
} else {
|
|
201
|
+
const occurrenceIndex = getComponentOccurrenceIndex(manifest, component)
|
|
202
|
+
refLineIndex = findComponentInvocationLine(lines, component.componentName, occurrenceIndex)
|
|
203
|
+
if (refLineIndex < 0) {
|
|
204
|
+
refLineIndex = component.sourceLine - 1
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (refLineIndex < 0 || refLineIndex >= lines.length) {
|
|
209
|
+
return { success: false, error: `Invalid source line for component: ${refLineIndex + 1}` }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const { startLine, endLine } = findComponentBounds(
|
|
213
|
+
lines,
|
|
214
|
+
refLineIndex,
|
|
215
|
+
component.componentName,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
let removeCount = endLine - startLine + 1
|
|
219
|
+
if (endLine + 1 < lines.length && lines[endLine + 1]!.trim() === '') {
|
|
220
|
+
removeCount++
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
lines.splice(startLine, removeCount)
|
|
224
|
+
await fs.writeFile(fullPath, lines.join('\n'), 'utf-8')
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
message: `Successfully removed ${component.componentName} component`,
|
|
229
|
+
sourceFile: filePath,
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
release()
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
236
|
+
return { success: false, error: message }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// --- Helper functions ported from CmsComponentHandler ---
|
|
241
|
+
|
|
242
|
+
function findComponentBounds(
|
|
243
|
+
lines: string[],
|
|
244
|
+
startLineIndex: number,
|
|
245
|
+
componentName: string,
|
|
246
|
+
): { startLine: number; endLine: number } {
|
|
247
|
+
const startLine = startLineIndex
|
|
248
|
+
|
|
249
|
+
// Check if the opening tag is self-closing (may span multiple lines)
|
|
250
|
+
// Scan from startLine forward until we find either '/>' or '>' to determine tag style
|
|
251
|
+
let tagClosed = false
|
|
252
|
+
for (let i = startLineIndex; i < lines.length; i++) {
|
|
253
|
+
const currentLine = lines[i]!
|
|
254
|
+
// Check for self-closing '/>' before any '>' on this line
|
|
255
|
+
const selfCloseIdx = currentLine.indexOf('/>')
|
|
256
|
+
const openEndIdx = currentLine.indexOf('>')
|
|
257
|
+
|
|
258
|
+
if (selfCloseIdx >= 0 && (openEndIdx < 0 || selfCloseIdx <= openEndIdx)) {
|
|
259
|
+
// Self-closing tag found
|
|
260
|
+
return { startLine, endLine: i }
|
|
261
|
+
}
|
|
262
|
+
if (openEndIdx >= 0) {
|
|
263
|
+
// Opening tag closed with '>' (not self-closing)
|
|
264
|
+
tagClosed = true
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// If the tag never closed, return just the start line
|
|
270
|
+
if (!tagClosed) {
|
|
271
|
+
return { startLine, endLine: startLineIndex }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const escapedName = escapeRegex(componentName)
|
|
275
|
+
const closingTag = `</${componentName}>`
|
|
276
|
+
let depth = 1
|
|
277
|
+
let endLine = startLineIndex
|
|
278
|
+
|
|
279
|
+
for (let i = startLineIndex + 1; i < lines.length; i++) {
|
|
280
|
+
const currentLine = lines[i]!
|
|
281
|
+
|
|
282
|
+
const openingMatches = currentLine.match(new RegExp(`<${escapedName}(?:\\s|>)`, 'g'))
|
|
283
|
+
if (openingMatches) {
|
|
284
|
+
for (const match of openingMatches) {
|
|
285
|
+
const tagStart = currentLine.indexOf(match)
|
|
286
|
+
const restOfTag = currentLine.slice(tagStart)
|
|
287
|
+
if (!restOfTag.includes('/>') || restOfTag.indexOf('/>') > restOfTag.indexOf('>')) {
|
|
288
|
+
depth++
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const closingMatches = currentLine.match(new RegExp(escapeRegex(closingTag), 'g'))
|
|
294
|
+
if (closingMatches) {
|
|
295
|
+
depth -= closingMatches.length
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (depth <= 0) {
|
|
299
|
+
endLine = i
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { startLine, endLine }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function getPageFileCandidates(pageUrl: string): string[] {
|
|
308
|
+
let pathname: string
|
|
309
|
+
try {
|
|
310
|
+
const url = new URL(pageUrl)
|
|
311
|
+
pathname = url.pathname
|
|
312
|
+
} catch {
|
|
313
|
+
pathname = pageUrl.split('?')[0]?.split('#')[0] ?? '/'
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (pathname.length > 1 && pathname.endsWith('/')) {
|
|
317
|
+
pathname = pathname.slice(0, -1)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (pathname === '/' || pathname === '') {
|
|
321
|
+
return ['src/pages/index.astro', 'src/pages/index.mdx']
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const p = pathname.slice(1)
|
|
325
|
+
return [
|
|
326
|
+
`src/pages/${p}.astro`,
|
|
327
|
+
`src/pages/${p}/index.astro`,
|
|
328
|
+
`src/pages/${p}.mdx`,
|
|
329
|
+
`src/pages/${p}/index.mdx`,
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getComponentOccurrenceIndex(
|
|
334
|
+
manifest: CmsManifest,
|
|
335
|
+
referenceComponent: ComponentInstance,
|
|
336
|
+
): number {
|
|
337
|
+
if (referenceComponent.invocationIndex !== undefined) {
|
|
338
|
+
return referenceComponent.invocationIndex
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const componentName = referenceComponent.componentName
|
|
342
|
+
const invocationSource = referenceComponent.invocationSourcePath
|
|
343
|
+
const sameNameComponents = Object.values(manifest.components)
|
|
344
|
+
.filter(c =>
|
|
345
|
+
c.componentName === componentName
|
|
346
|
+
&& (!invocationSource || c.invocationSourcePath === invocationSource),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
const index = sameNameComponents.findIndex(c => c.id === referenceComponent.id)
|
|
350
|
+
return index >= 0 ? index : 0
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function findComponentInvocationFile(
|
|
354
|
+
projectRoot: string,
|
|
355
|
+
pageUrl: string,
|
|
356
|
+
manifest: CmsManifest,
|
|
357
|
+
referenceComponent: ComponentInstance,
|
|
358
|
+
): Promise<{ filePath: string; lineIndex: number } | null> {
|
|
359
|
+
// If manifest provides invocationSourcePath, use it directly
|
|
360
|
+
if (referenceComponent.invocationSourcePath) {
|
|
361
|
+
const filePath = normalizeFilePath(referenceComponent.invocationSourcePath)
|
|
362
|
+
const fullPath = path.resolve(projectRoot, filePath)
|
|
363
|
+
try {
|
|
364
|
+
const content = await fs.readFile(fullPath, 'utf-8')
|
|
365
|
+
const lines = content.split('\n')
|
|
366
|
+
const lineIndex = findComponentInvocationLine(
|
|
367
|
+
lines,
|
|
368
|
+
referenceComponent.componentName,
|
|
369
|
+
referenceComponent.invocationIndex ?? 0,
|
|
370
|
+
)
|
|
371
|
+
if (lineIndex >= 0) {
|
|
372
|
+
return { filePath, lineIndex }
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
// File not found, fall through to candidates
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Derive page file path from URL and search for the component invocation
|
|
380
|
+
const candidates = getPageFileCandidates(pageUrl)
|
|
381
|
+
const occurrenceIndex = getComponentOccurrenceIndex(manifest, referenceComponent)
|
|
382
|
+
|
|
383
|
+
for (const candidate of candidates) {
|
|
384
|
+
const fullPath = path.resolve(projectRoot, candidate)
|
|
385
|
+
try {
|
|
386
|
+
const content = await fs.readFile(fullPath, 'utf-8')
|
|
387
|
+
const lines = content.split('\n')
|
|
388
|
+
const lineIndex = findComponentInvocationLine(
|
|
389
|
+
lines,
|
|
390
|
+
referenceComponent.componentName,
|
|
391
|
+
occurrenceIndex,
|
|
392
|
+
)
|
|
393
|
+
if (lineIndex >= 0) {
|
|
394
|
+
return { filePath: candidate, lineIndex }
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
// File not found, try next candidate
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return null
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function findComponentInvocationLine(
|
|
405
|
+
lines: string[],
|
|
406
|
+
componentName: string,
|
|
407
|
+
occurrenceIndex: number,
|
|
408
|
+
): number {
|
|
409
|
+
const pattern = new RegExp(`<${escapeRegex(componentName)}(?:\\s|>|/>)`)
|
|
410
|
+
let found = 0
|
|
411
|
+
for (let i = 0; i < lines.length; i++) {
|
|
412
|
+
if (pattern.test(lines[i]!)) {
|
|
413
|
+
if (found === occurrenceIndex) return i
|
|
414
|
+
found++
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return found > 0 ? findComponentInvocationLine(lines, componentName, 0) : -1
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function generateComponentJsx(
|
|
421
|
+
componentName: string,
|
|
422
|
+
props: Record<string, unknown>,
|
|
423
|
+
_definition: ComponentDefinition,
|
|
424
|
+
): string {
|
|
425
|
+
const propsString = Object.entries(props)
|
|
426
|
+
.map(([key, value]) => {
|
|
427
|
+
if (typeof value === 'string') {
|
|
428
|
+
return `${key}="${escapeHtml(value)}"`
|
|
429
|
+
}
|
|
430
|
+
if (typeof value === 'boolean') {
|
|
431
|
+
return value ? key : `${key}={false}`
|
|
432
|
+
}
|
|
433
|
+
if (typeof value === 'number') {
|
|
434
|
+
return `${key}={${value}}`
|
|
435
|
+
}
|
|
436
|
+
return `${key}={${JSON.stringify(value)}}`
|
|
437
|
+
})
|
|
438
|
+
.join(' ')
|
|
439
|
+
|
|
440
|
+
if (propsString) {
|
|
441
|
+
return `<${componentName} ${propsString} />`
|
|
442
|
+
}
|
|
443
|
+
return `<${componentName} />`
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function escapeHtml(str: string): string {
|
|
447
|
+
return str
|
|
448
|
+
.replace(/&/g, '&')
|
|
449
|
+
.replace(/"/g, '"')
|
|
450
|
+
.replace(/'/g, ''')
|
|
451
|
+
.replace(/</g, '<')
|
|
452
|
+
.replace(/>/g, '>')
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function getIndentation(line: string): string {
|
|
456
|
+
const match = line.match(/^(\s*)/)
|
|
457
|
+
return match ? match[1]! : ''
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function normalizeFilePath(p: string): string {
|
|
461
|
+
return p.startsWith('/') ? p.slice(1) : p
|
|
462
|
+
}
|
|
463
|
+
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import yaml from 'yaml'
|
|
4
|
+
import { getProjectRoot } from '../config'
|
|
5
|
+
import { acquireFileLock, resolveAndValidatePath as sharedResolveAndValidatePath } from '../utils'
|
|
6
|
+
|
|
7
|
+
export interface BlogFrontmatter {
|
|
8
|
+
title: string
|
|
9
|
+
date: string
|
|
10
|
+
author?: string
|
|
11
|
+
categories?: string[]
|
|
12
|
+
excerpt?: string
|
|
13
|
+
featuredImage?: string
|
|
14
|
+
draft?: boolean
|
|
15
|
+
[key: string]: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateMarkdownRequest {
|
|
19
|
+
collection: string
|
|
20
|
+
title: string
|
|
21
|
+
slug: string
|
|
22
|
+
frontmatter?: Partial<BlogFrontmatter>
|
|
23
|
+
content?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CreateMarkdownResponse {
|
|
27
|
+
success: boolean
|
|
28
|
+
filePath?: string
|
|
29
|
+
slug?: string
|
|
30
|
+
error?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UpdateMarkdownRequest {
|
|
34
|
+
filePath: string
|
|
35
|
+
frontmatter?: Partial<BlogFrontmatter>
|
|
36
|
+
content?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UpdateMarkdownResponse {
|
|
40
|
+
success: boolean
|
|
41
|
+
error?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GetMarkdownContentResponse {
|
|
45
|
+
content: string
|
|
46
|
+
frontmatter: BlogFrontmatter
|
|
47
|
+
filePath: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function handleGetMarkdownContent(
|
|
51
|
+
filePath: string,
|
|
52
|
+
): Promise<GetMarkdownContentResponse | null> {
|
|
53
|
+
try {
|
|
54
|
+
const fullPath = resolveAndValidatePath(filePath)
|
|
55
|
+
const raw = await fs.readFile(fullPath, 'utf-8')
|
|
56
|
+
const { frontmatter, content } = parseFrontmatter(raw)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
content,
|
|
60
|
+
frontmatter: frontmatter as BlogFrontmatter,
|
|
61
|
+
filePath,
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function handleUpdateMarkdown(
|
|
69
|
+
request: UpdateMarkdownRequest,
|
|
70
|
+
): Promise<UpdateMarkdownResponse> {
|
|
71
|
+
try {
|
|
72
|
+
const fullPath = resolveAndValidatePath(request.filePath)
|
|
73
|
+
const release = await acquireFileLock(fullPath)
|
|
74
|
+
try {
|
|
75
|
+
const raw = await fs.readFile(fullPath, 'utf-8')
|
|
76
|
+
const existing = parseFrontmatter(raw)
|
|
77
|
+
|
|
78
|
+
const mergedFrontmatter: BlogFrontmatter = {
|
|
79
|
+
...(existing.frontmatter as BlogFrontmatter),
|
|
80
|
+
...request.frontmatter,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const finalContent = request.content ?? existing.content
|
|
84
|
+
const markdownContent = serializeFrontmatter(mergedFrontmatter, finalContent)
|
|
85
|
+
|
|
86
|
+
await fs.writeFile(fullPath, markdownContent, 'utf-8')
|
|
87
|
+
|
|
88
|
+
return { success: true }
|
|
89
|
+
} finally {
|
|
90
|
+
release()
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
94
|
+
return { success: false, error: message }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function handleCreateMarkdown(
|
|
99
|
+
request: CreateMarkdownRequest,
|
|
100
|
+
): Promise<CreateMarkdownResponse> {
|
|
101
|
+
const { collection, title, slug, frontmatter = {}, content = '' } = request
|
|
102
|
+
|
|
103
|
+
const normalizedSlug = slugify(slug || title)
|
|
104
|
+
if (!normalizedSlug) {
|
|
105
|
+
return { success: false, error: 'Could not generate a valid slug from the provided title/slug' }
|
|
106
|
+
}
|
|
107
|
+
const filePath = `src/content/${collection}/${normalizedSlug}.md`
|
|
108
|
+
const fullPath = resolveAndValidatePath(filePath)
|
|
109
|
+
|
|
110
|
+
const fullFrontmatter: BlogFrontmatter = {
|
|
111
|
+
title,
|
|
112
|
+
date: new Date().toISOString().split('T')[0]!,
|
|
113
|
+
draft: true,
|
|
114
|
+
...frontmatter,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const markdownContent = serializeFrontmatter(fullFrontmatter, content)
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true })
|
|
121
|
+
// Use 'wx' flag for atomic exclusive create — fails if file already exists
|
|
122
|
+
await fs.writeFile(fullPath, markdownContent, { encoding: 'utf-8', flag: 'wx' })
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
filePath,
|
|
127
|
+
slug: normalizedSlug,
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
131
|
+
return { success: false, error: `File already exists: ${filePath}` }
|
|
132
|
+
}
|
|
133
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
134
|
+
return { success: false, error: message }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Internal helpers ---
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a user-provided file path and ensure it stays within the project root.
|
|
142
|
+
* Throws if the resolved path escapes the project boundary.
|
|
143
|
+
*/
|
|
144
|
+
function resolveAndValidatePath(filePath: string): string {
|
|
145
|
+
const projectRoot = getProjectRoot()
|
|
146
|
+
const normalizedPath = filePath.startsWith('/') ? filePath.slice(1) : filePath
|
|
147
|
+
const fullPath = path.resolve(projectRoot, normalizedPath)
|
|
148
|
+
|
|
149
|
+
// Ensure the resolved path is within the project root
|
|
150
|
+
const resolvedRoot = path.resolve(projectRoot)
|
|
151
|
+
if (!fullPath.startsWith(resolvedRoot + path.sep) && fullPath !== resolvedRoot) {
|
|
152
|
+
throw new Error(`Path traversal detected: ${filePath}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return fullPath
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseFrontmatter(raw: string): { frontmatter: Record<string, unknown>; content: string } {
|
|
159
|
+
const trimmed = raw.trimStart()
|
|
160
|
+
if (!trimmed.startsWith('---')) {
|
|
161
|
+
return { frontmatter: {}, content: raw }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Find closing --- on its own line (not inside YAML values)
|
|
165
|
+
const lines = trimmed.split('\n')
|
|
166
|
+
let endLineIndex = -1
|
|
167
|
+
for (let i = 1; i < lines.length; i++) {
|
|
168
|
+
if (lines[i]!.trimEnd() === '---') {
|
|
169
|
+
endLineIndex = i
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (endLineIndex === -1) {
|
|
174
|
+
return { frontmatter: {}, content: raw }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const yamlStr = lines.slice(1, endLineIndex).join('\n').trim()
|
|
178
|
+
const content = lines.slice(endLineIndex + 1).join('\n').replace(/^\r?\n/, '')
|
|
179
|
+
|
|
180
|
+
let frontmatter: Record<string, unknown> = {}
|
|
181
|
+
try {
|
|
182
|
+
frontmatter = (yaml.parse(yamlStr) as Record<string, unknown>) ?? {}
|
|
183
|
+
} catch {
|
|
184
|
+
// Invalid YAML, return empty frontmatter
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { frontmatter, content }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function serializeFrontmatter(frontmatter: Record<string, unknown>, content: string): string {
|
|
191
|
+
const yamlStr = yaml.stringify(frontmatter).trim()
|
|
192
|
+
return `---\n${yamlStr}\n---\n${content}`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function slugify(text: string): string {
|
|
196
|
+
return text
|
|
197
|
+
.toLowerCase()
|
|
198
|
+
.trim()
|
|
199
|
+
.replace(/[^\w\s-]/g, '')
|
|
200
|
+
.replace(/[\s_-]+/g, '-')
|
|
201
|
+
.replace(/^-+|-+$/g, '')
|
|
202
|
+
}
|