@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,649 @@
|
|
|
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, ManifestEntry } from '../types'
|
|
6
|
+
import { acquireFileLock, escapeReplacement, normalizePagePath, resolveAndValidatePath } from '../utils'
|
|
7
|
+
|
|
8
|
+
export interface ColorChangePayload {
|
|
9
|
+
oldClass: string
|
|
10
|
+
newClass: string
|
|
11
|
+
type: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText'
|
|
12
|
+
sourcePath?: string
|
|
13
|
+
sourceLine?: number
|
|
14
|
+
sourceSnippet?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ImageChangePayload {
|
|
18
|
+
newSrc: string
|
|
19
|
+
newAlt: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AttributeChangePayload {
|
|
23
|
+
attributeName: string
|
|
24
|
+
oldValue: string | undefined
|
|
25
|
+
newValue: string | undefined
|
|
26
|
+
sourcePath?: string
|
|
27
|
+
sourceLine?: number
|
|
28
|
+
sourceSnippet?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ChangePayload {
|
|
32
|
+
cmsId: string
|
|
33
|
+
newValue: string
|
|
34
|
+
originalValue: string
|
|
35
|
+
sourcePath: string
|
|
36
|
+
sourceLine: number
|
|
37
|
+
sourceSnippet: string
|
|
38
|
+
htmlValue?: string
|
|
39
|
+
childCmsIds?: string[]
|
|
40
|
+
hasStyledContent?: boolean
|
|
41
|
+
colorChange?: ColorChangePayload
|
|
42
|
+
imageChange?: ImageChangePayload
|
|
43
|
+
attributeChanges?: AttributeChangePayload[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SaveBatchRequest {
|
|
47
|
+
changes: ChangePayload[]
|
|
48
|
+
meta: { source: string; url: string }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SaveBatchResponse {
|
|
52
|
+
updated: number
|
|
53
|
+
errors?: Array<{ cmsId: string; error: string }>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function handleUpdate(
|
|
57
|
+
request: SaveBatchRequest,
|
|
58
|
+
manifestWriter: ManifestWriter,
|
|
59
|
+
): Promise<SaveBatchResponse> {
|
|
60
|
+
const { changes, meta } = request
|
|
61
|
+
const errors: Array<{ cmsId: string; error: string }> = []
|
|
62
|
+
let updated = 0
|
|
63
|
+
|
|
64
|
+
// Get the manifest for the page being edited
|
|
65
|
+
const pagePath = normalizePagePath(meta.url)
|
|
66
|
+
const pageData = manifestWriter.getPageManifest(pagePath)
|
|
67
|
+
const manifest: CmsManifest = pageData
|
|
68
|
+
? {
|
|
69
|
+
entries: pageData.entries,
|
|
70
|
+
components: pageData.components,
|
|
71
|
+
componentDefinitions: manifestWriter.getComponentDefinitions(),
|
|
72
|
+
}
|
|
73
|
+
: manifestWriter.getGlobalManifest()
|
|
74
|
+
|
|
75
|
+
// Group changes by source file
|
|
76
|
+
const changesByFile: Record<string, ChangePayload[]> = {}
|
|
77
|
+
for (const change of changes) {
|
|
78
|
+
const filePath = change.sourcePath
|
|
79
|
+
if (!filePath) {
|
|
80
|
+
errors.push({ cmsId: change.cmsId, error: 'No file path in change payload' })
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if (!changesByFile[filePath]) {
|
|
84
|
+
changesByFile[filePath] = []
|
|
85
|
+
}
|
|
86
|
+
changesByFile[filePath]!.push(change)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const projectRoot = getProjectRoot()
|
|
90
|
+
|
|
91
|
+
for (const [filePath, fileChanges] of Object.entries(changesByFile)) {
|
|
92
|
+
try {
|
|
93
|
+
const fullPath = resolveAndValidatePath(filePath)
|
|
94
|
+
const release = await acquireFileLock(fullPath)
|
|
95
|
+
try {
|
|
96
|
+
const currentContent = await fs.readFile(fullPath, 'utf-8')
|
|
97
|
+
|
|
98
|
+
const { newContent, appliedCount, failedChanges } = applyChanges(
|
|
99
|
+
currentContent,
|
|
100
|
+
fileChanges,
|
|
101
|
+
manifest,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if (failedChanges.length > 0) {
|
|
105
|
+
errors.push(...failedChanges)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (appliedCount > 0 && newContent !== currentContent) {
|
|
109
|
+
await fs.writeFile(fullPath, newContent, 'utf-8')
|
|
110
|
+
updated += appliedCount
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
release()
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
117
|
+
errors.push(
|
|
118
|
+
...fileChanges.map((c) => ({ cmsId: c.cmsId, error: errorMessage })),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
updated,
|
|
125
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function applyChanges(
|
|
130
|
+
content: string,
|
|
131
|
+
changes: ChangePayload[],
|
|
132
|
+
manifest: CmsManifest,
|
|
133
|
+
): {
|
|
134
|
+
newContent: string
|
|
135
|
+
appliedCount: number
|
|
136
|
+
failedChanges: Array<{ cmsId: string; error: string }>
|
|
137
|
+
} {
|
|
138
|
+
let newContent = content
|
|
139
|
+
let appliedCount = 0
|
|
140
|
+
const failedChanges: Array<{ cmsId: string; error: string }> = []
|
|
141
|
+
|
|
142
|
+
// Sort changes by source line descending to prevent offset shifts
|
|
143
|
+
const sortedChanges = [...changes].sort(
|
|
144
|
+
(a, b) => (b.sourceLine ?? 0) - (a.sourceLine ?? 0),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
for (const change of sortedChanges) {
|
|
148
|
+
// Handle image changes
|
|
149
|
+
if (change.imageChange) {
|
|
150
|
+
const result = applyImageChange(newContent, change)
|
|
151
|
+
if (result.success) {
|
|
152
|
+
newContent = result.content
|
|
153
|
+
appliedCount++
|
|
154
|
+
} else {
|
|
155
|
+
failedChanges.push({ cmsId: change.cmsId, error: result.error })
|
|
156
|
+
}
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle color class changes
|
|
161
|
+
if (change.colorChange) {
|
|
162
|
+
const result = applyColorChange(newContent, change)
|
|
163
|
+
if (result.success) {
|
|
164
|
+
newContent = result.content
|
|
165
|
+
appliedCount++
|
|
166
|
+
} else {
|
|
167
|
+
failedChanges.push({ cmsId: change.cmsId, error: result.error })
|
|
168
|
+
}
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Handle attribute changes
|
|
173
|
+
if (change.attributeChanges && change.attributeChanges.length > 0) {
|
|
174
|
+
const result = applyAttributeChanges(newContent, change)
|
|
175
|
+
if (result.appliedCount > 0) {
|
|
176
|
+
newContent = result.content
|
|
177
|
+
appliedCount++
|
|
178
|
+
}
|
|
179
|
+
failedChanges.push(...result.failedChanges)
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Text content change
|
|
184
|
+
const result = applyTextChange(newContent, change, manifest)
|
|
185
|
+
if (result.success) {
|
|
186
|
+
newContent = result.content
|
|
187
|
+
appliedCount++
|
|
188
|
+
} else {
|
|
189
|
+
failedChanges.push({ cmsId: change.cmsId, error: result.error })
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { newContent, appliedCount, failedChanges }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function applyImageChange(
|
|
197
|
+
content: string,
|
|
198
|
+
change: ChangePayload,
|
|
199
|
+
): { success: true; content: string } | { success: false; error: string } {
|
|
200
|
+
const { newSrc, newAlt } = change.imageChange!
|
|
201
|
+
const originalSrc = change.originalValue
|
|
202
|
+
|
|
203
|
+
if (!originalSrc) {
|
|
204
|
+
return { success: false, error: 'No original image src in change payload' }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const srcCandidates = [originalSrc]
|
|
208
|
+
if (originalSrc.startsWith('http://') || originalSrc.startsWith('https://')) {
|
|
209
|
+
try {
|
|
210
|
+
const parsedUrl = new URL(originalSrc)
|
|
211
|
+
if (parsedUrl.pathname !== originalSrc) {
|
|
212
|
+
srcCandidates.push(parsedUrl.pathname)
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// URL parsing failed, just use original value
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Extract the authored src from the source snippet if available
|
|
220
|
+
// This handles cases where an Image component transforms the URL (e.g., CDN optimization)
|
|
221
|
+
// so the rendered src differs from the authored src in the source file
|
|
222
|
+
if (change.sourceSnippet) {
|
|
223
|
+
const snippetSrcMatch = change.sourceSnippet.match(/src\s*=\s*"([^"]+)"/) || change.sourceSnippet.match(/src\s*=\s*'([^']+)'/)
|
|
224
|
+
if (snippetSrcMatch?.[1] && !srcCandidates.includes(snippetSrcMatch[1])) {
|
|
225
|
+
srcCandidates.push(snippetSrcMatch[1])
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let newContent = content
|
|
230
|
+
let replacedIndex = -1
|
|
231
|
+
for (const srcToFind of srcCandidates) {
|
|
232
|
+
// Use non-global patterns to replace only the first occurrence
|
|
233
|
+
const srcPatternDouble = new RegExp(`src="${escapeRegExp(srcToFind)}"`)
|
|
234
|
+
const srcPatternSingle = new RegExp(`src='${escapeRegExp(srcToFind)}'`)
|
|
235
|
+
|
|
236
|
+
const escapedNewSrc = escapeReplacement(newSrc)
|
|
237
|
+
const doubleMatch = newContent.match(srcPatternDouble)
|
|
238
|
+
if (doubleMatch && doubleMatch.index !== undefined) {
|
|
239
|
+
replacedIndex = doubleMatch.index
|
|
240
|
+
newContent = newContent.slice(0, replacedIndex)
|
|
241
|
+
+ newContent.slice(replacedIndex).replace(srcPatternDouble, `src="${escapedNewSrc}"`)
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
const singleMatch = newContent.match(srcPatternSingle)
|
|
245
|
+
if (singleMatch && singleMatch.index !== undefined) {
|
|
246
|
+
replacedIndex = singleMatch.index
|
|
247
|
+
newContent = newContent.slice(0, replacedIndex)
|
|
248
|
+
+ newContent.slice(replacedIndex).replace(srcPatternSingle, `src='${escapedNewSrc}'`)
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Fallback: if literal src not found, try to find an expression-based src attribute
|
|
254
|
+
// near the source line (handles src={variable}, src={obj.prop}, etc.)
|
|
255
|
+
if (replacedIndex < 0 && change.sourceLine > 0) {
|
|
256
|
+
const lines = newContent.split('\n')
|
|
257
|
+
const targetLineIdx = change.sourceLine - 1
|
|
258
|
+
|
|
259
|
+
// Search a region around the source line for an <img with src attribute
|
|
260
|
+
const regionStart = Math.max(0, targetLineIdx - 3)
|
|
261
|
+
const regionEnd = Math.min(lines.length, targetLineIdx + 10)
|
|
262
|
+
const regionLines = lines.slice(regionStart, regionEnd)
|
|
263
|
+
const regionText = regionLines.join('\n')
|
|
264
|
+
|
|
265
|
+
// Verify we're in an img or Image component context before replacing
|
|
266
|
+
if (/<img\b/i.test(regionText) || /<Image\b/.test(regionText)) {
|
|
267
|
+
// Match src attribute with expression value: src={...} (handling balanced braces)
|
|
268
|
+
const exprMatch = findExpressionSrcAttribute(regionText)
|
|
269
|
+
if (exprMatch) {
|
|
270
|
+
const regionOffset = regionStart > 0
|
|
271
|
+
? lines.slice(0, regionStart).join('\n').length + 1
|
|
272
|
+
: 0
|
|
273
|
+
const absoluteIndex = regionOffset + exprMatch.index
|
|
274
|
+
|
|
275
|
+
const escapedNewSrc = escapeReplacement(newSrc)
|
|
276
|
+
newContent = newContent.slice(0, absoluteIndex)
|
|
277
|
+
+ `src="${escapedNewSrc}"`
|
|
278
|
+
+ newContent.slice(absoluteIndex + exprMatch.length)
|
|
279
|
+
replacedIndex = absoluteIndex
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (replacedIndex < 0) {
|
|
285
|
+
return { success: false, error: `Image src not found in source file: ${originalSrc}` }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Replace alt only in the same img tag context (within ~500 chars around the replaced src)
|
|
289
|
+
if (newAlt !== undefined) {
|
|
290
|
+
const searchStart = Math.max(0, replacedIndex - 200)
|
|
291
|
+
const searchEnd = Math.min(newContent.length, replacedIndex + 300)
|
|
292
|
+
const region = newContent.slice(searchStart, searchEnd)
|
|
293
|
+
|
|
294
|
+
const altPatternDouble = /alt="[^"]*"/
|
|
295
|
+
const altPatternSingle = /alt='[^']*'/
|
|
296
|
+
// Also match expression-based alt: alt={...}
|
|
297
|
+
const altPatternExpr = /alt\s*=\s*\{[^}]*\}/
|
|
298
|
+
|
|
299
|
+
const altDoubleMatch = region.match(altPatternDouble)
|
|
300
|
+
const altSingleMatch = region.match(altPatternSingle)
|
|
301
|
+
const altExprMatch = region.match(altPatternExpr)
|
|
302
|
+
|
|
303
|
+
// Pick the first match found (string literals preferred over expressions)
|
|
304
|
+
const altMatch = altDoubleMatch ?? altSingleMatch ?? altExprMatch
|
|
305
|
+
const altQuote = altDoubleMatch ? '"' : altSingleMatch ? "'" : '"'
|
|
306
|
+
|
|
307
|
+
if (altMatch && altMatch.index !== undefined) {
|
|
308
|
+
const altAbsoluteIndex = searchStart + altMatch.index
|
|
309
|
+
// Escape quotes in alt text matching the quote style used
|
|
310
|
+
const escapedAlt = altQuote === '"'
|
|
311
|
+
? newAlt.replace(/"/g, '"')
|
|
312
|
+
: newAlt.replace(/'/g, ''')
|
|
313
|
+
newContent = newContent.slice(0, altAbsoluteIndex)
|
|
314
|
+
+ `alt=${altQuote}${escapedAlt}${altQuote}`
|
|
315
|
+
+ newContent.slice(altAbsoluteIndex + altMatch[0].length)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { success: true, content: newContent }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function applyColorChange(
|
|
323
|
+
content: string,
|
|
324
|
+
change: ChangePayload,
|
|
325
|
+
): { success: true; content: string } | { success: false; error: string } {
|
|
326
|
+
const { oldClass, newClass } = change.colorChange!
|
|
327
|
+
// Prefer colorChange's own sourceLine (points to the class attribute)
|
|
328
|
+
// over the outer change.sourceLine (may point to a data declaration)
|
|
329
|
+
const sourceLine = change.colorChange!.sourceLine ?? change.sourceLine
|
|
330
|
+
|
|
331
|
+
// When oldClass is empty, we're adding a new color class (not replacing)
|
|
332
|
+
if (!oldClass) {
|
|
333
|
+
return appendClassToAttribute(content, newClass, sourceLine)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return replaceClassInAttribute(content, oldClass, newClass, sourceLine)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Replace an existing class within a class attribute by splitting on whitespace.
|
|
341
|
+
* This avoids \b word-boundary issues (e.g., \b matching `:` in `hover:bg-red-500`).
|
|
342
|
+
*/
|
|
343
|
+
function replaceClassInAttribute(
|
|
344
|
+
content: string,
|
|
345
|
+
oldClass: string,
|
|
346
|
+
newClass: string,
|
|
347
|
+
sourceLine?: number,
|
|
348
|
+
): { success: true; content: string } | { success: false; error: string } {
|
|
349
|
+
const classAttrPattern = /(class\s*=\s*)(["'])([^"']*)\2/
|
|
350
|
+
|
|
351
|
+
const replaceOnLine = (line: string): string | null => {
|
|
352
|
+
const match = line.match(classAttrPattern)
|
|
353
|
+
if (!match) return null
|
|
354
|
+
|
|
355
|
+
const prefix = match[1]!
|
|
356
|
+
const quote = match[2]!
|
|
357
|
+
const classContent = match[3]!
|
|
358
|
+
|
|
359
|
+
const classes = classContent.split(/\s+/).filter(Boolean)
|
|
360
|
+
const idx = classes.indexOf(oldClass)
|
|
361
|
+
if (idx === -1) return null
|
|
362
|
+
|
|
363
|
+
classes[idx] = newClass
|
|
364
|
+
return line.replace(classAttrPattern, `${prefix}${quote}${classes.join(' ')}${quote}`)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (sourceLine) {
|
|
368
|
+
const lines = content.split('\n')
|
|
369
|
+
const lineIndex = sourceLine - 1
|
|
370
|
+
|
|
371
|
+
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
372
|
+
const result = replaceOnLine(lines[lineIndex]!)
|
|
373
|
+
if (result !== null) {
|
|
374
|
+
lines[lineIndex] = result
|
|
375
|
+
return { success: true, content: lines.join('\n') }
|
|
376
|
+
}
|
|
377
|
+
return { success: false, error: `Color class '${oldClass}' not found on line ${sourceLine}` }
|
|
378
|
+
}
|
|
379
|
+
return { success: false, error: `Invalid source line ${sourceLine}` }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Fallback: find the first class attribute in the content that contains oldClass
|
|
383
|
+
const lines = content.split('\n')
|
|
384
|
+
for (let i = 0; i < lines.length; i++) {
|
|
385
|
+
const result = replaceOnLine(lines[i]!)
|
|
386
|
+
if (result !== null) {
|
|
387
|
+
lines[i] = result
|
|
388
|
+
return { success: true, content: lines.join('\n') }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { success: false, error: `Color class '${oldClass}' not found in source file` }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Append a new class to an existing class attribute.
|
|
396
|
+
*/
|
|
397
|
+
function appendClassToAttribute(
|
|
398
|
+
content: string,
|
|
399
|
+
newClass: string,
|
|
400
|
+
sourceLine?: number,
|
|
401
|
+
): { success: true; content: string } | { success: false; error: string } {
|
|
402
|
+
const appendPattern = /(class\s*=\s*["'])([^"']*)(["'])/
|
|
403
|
+
|
|
404
|
+
const doAppend = (_: string, open: string, classes: string, close: string) => {
|
|
405
|
+
const trimmed = classes.trimEnd()
|
|
406
|
+
const separator = trimmed ? ' ' : ''
|
|
407
|
+
return `${open}${trimmed}${separator}${escapeReplacement(newClass)}${close}`
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (sourceLine) {
|
|
411
|
+
const lines = content.split('\n')
|
|
412
|
+
const lineIndex = sourceLine - 1
|
|
413
|
+
|
|
414
|
+
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
415
|
+
const line = lines[lineIndex]!
|
|
416
|
+
if (appendPattern.test(line)) {
|
|
417
|
+
lines[lineIndex] = line.replace(appendPattern, doAppend)
|
|
418
|
+
return { success: true, content: lines.join('\n') }
|
|
419
|
+
}
|
|
420
|
+
return { success: false, error: `No class attribute found on line ${sourceLine}` }
|
|
421
|
+
}
|
|
422
|
+
return { success: false, error: `Invalid source line ${sourceLine}` }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (appendPattern.test(content)) {
|
|
426
|
+
return {
|
|
427
|
+
success: true,
|
|
428
|
+
content: content.replace(appendPattern, doAppend),
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return { success: false, error: 'No class attribute found in source file' }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function applyAttributeChanges(
|
|
435
|
+
content: string,
|
|
436
|
+
change: ChangePayload,
|
|
437
|
+
): {
|
|
438
|
+
content: string
|
|
439
|
+
appliedCount: number
|
|
440
|
+
failedChanges: Array<{ cmsId: string; error: string }>
|
|
441
|
+
} {
|
|
442
|
+
let newContent = content
|
|
443
|
+
let attrApplied = 0
|
|
444
|
+
const failedChanges: Array<{ cmsId: string; error: string }> = []
|
|
445
|
+
|
|
446
|
+
for (const attrChange of change.attributeChanges!) {
|
|
447
|
+
const { attributeName, oldValue: attrOldValue, newValue: attrNewValue } = attrChange
|
|
448
|
+
if (attrOldValue === undefined || attrNewValue === undefined) {
|
|
449
|
+
failedChanges.push({
|
|
450
|
+
cmsId: change.cmsId,
|
|
451
|
+
error: `Missing oldValue or newValue for attribute '${attributeName}'`,
|
|
452
|
+
})
|
|
453
|
+
continue
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const targetLine = attrChange.sourceLine ?? change.sourceLine
|
|
457
|
+
if (targetLine) {
|
|
458
|
+
const lines = newContent.split('\n')
|
|
459
|
+
const lineIndex = targetLine - 1
|
|
460
|
+
|
|
461
|
+
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
462
|
+
const line = lines[lineIndex]!
|
|
463
|
+
const doubleQuotePattern = new RegExp(
|
|
464
|
+
`(${escapeRegExp(attributeName)}\\s*=\\s*)"(${escapeRegExp(attrOldValue)})"`,
|
|
465
|
+
)
|
|
466
|
+
const singleQuotePattern = new RegExp(
|
|
467
|
+
`(${escapeRegExp(attributeName)}\\s*=\\s*)'(${escapeRegExp(attrOldValue)})'`,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
const safeNewValue = escapeReplacement(attrNewValue)
|
|
471
|
+
if (doubleQuotePattern.test(line)) {
|
|
472
|
+
lines[lineIndex] = line.replace(doubleQuotePattern, `$1"${safeNewValue}"`)
|
|
473
|
+
newContent = lines.join('\n')
|
|
474
|
+
attrApplied++
|
|
475
|
+
} else if (singleQuotePattern.test(line)) {
|
|
476
|
+
lines[lineIndex] = line.replace(singleQuotePattern, `$1'${safeNewValue}'`)
|
|
477
|
+
newContent = lines.join('\n')
|
|
478
|
+
attrApplied++
|
|
479
|
+
} else {
|
|
480
|
+
failedChanges.push({
|
|
481
|
+
cmsId: change.cmsId,
|
|
482
|
+
error: `Attribute '${attributeName}="${attrOldValue}"' not found on line ${targetLine}`,
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
failedChanges.push({
|
|
487
|
+
cmsId: change.cmsId,
|
|
488
|
+
error: `Invalid source line ${targetLine} for attribute '${attributeName}'`,
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
// Fallback: replace first occurrence in the whole file
|
|
493
|
+
const doubleQuotePattern = new RegExp(
|
|
494
|
+
`(${escapeRegExp(attributeName)}\\s*=\\s*)"(${escapeRegExp(attrOldValue)})"`,
|
|
495
|
+
)
|
|
496
|
+
const singleQuotePattern = new RegExp(
|
|
497
|
+
`(${escapeRegExp(attributeName)}\\s*=\\s*)'(${escapeRegExp(attrOldValue)})'`,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
const safeNewValue = escapeReplacement(attrNewValue)
|
|
501
|
+
if (doubleQuotePattern.test(newContent)) {
|
|
502
|
+
newContent = newContent.replace(doubleQuotePattern, `$1"${safeNewValue}"`)
|
|
503
|
+
attrApplied++
|
|
504
|
+
} else if (singleQuotePattern.test(newContent)) {
|
|
505
|
+
newContent = newContent.replace(singleQuotePattern, `$1'${safeNewValue}'`)
|
|
506
|
+
attrApplied++
|
|
507
|
+
} else {
|
|
508
|
+
failedChanges.push({
|
|
509
|
+
cmsId: change.cmsId,
|
|
510
|
+
error: `Attribute '${attributeName}="${attrOldValue}"' not found in source file`,
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return { content: newContent, appliedCount: attrApplied, failedChanges }
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function applyTextChange(
|
|
520
|
+
content: string,
|
|
521
|
+
change: ChangePayload,
|
|
522
|
+
manifest: CmsManifest,
|
|
523
|
+
): { success: true; content: string } | { success: false; error: string } {
|
|
524
|
+
const { sourceSnippet, originalValue, newValue, htmlValue } = change
|
|
525
|
+
|
|
526
|
+
let newText = htmlValue ?? newValue
|
|
527
|
+
newText = resolveCmsPlaceholders(newText, manifest)
|
|
528
|
+
|
|
529
|
+
if (!sourceSnippet || !originalValue) {
|
|
530
|
+
if (change.attributeChanges && change.attributeChanges.length > 0) {
|
|
531
|
+
return { success: true, content }
|
|
532
|
+
}
|
|
533
|
+
return { success: false, error: 'Missing sourceSnippet or originalValue in change payload' }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (!content.includes(sourceSnippet)) {
|
|
537
|
+
return { success: false, error: 'Source snippet not found in file' }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Replace originalValue with newText WITHIN the sourceSnippet
|
|
541
|
+
const updatedSnippet = sourceSnippet.replace(originalValue, newText)
|
|
542
|
+
|
|
543
|
+
if (updatedSnippet === sourceSnippet) {
|
|
544
|
+
// originalValue wasn't found in snippet - try HTML entity handling
|
|
545
|
+
const matchedText = findTextInSnippet(sourceSnippet, originalValue)
|
|
546
|
+
if (matchedText) {
|
|
547
|
+
const updatedWithEntity = sourceSnippet.replace(matchedText, newText)
|
|
548
|
+
return { success: true, content: content.replace(sourceSnippet, updatedWithEntity) }
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
success: false,
|
|
552
|
+
error: `Original text "${originalValue.substring(0, 50)}..." not found in source snippet`,
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return { success: true, content: content.replace(sourceSnippet, updatedSnippet) }
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Find the original text within a source snippet, accounting for HTML entities.
|
|
561
|
+
*/
|
|
562
|
+
function findTextInSnippet(snippet: string, decodedText: string): string | null {
|
|
563
|
+
if (snippet.includes(decodedText)) {
|
|
564
|
+
return decodedText
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const entityMap: Array<[string, string]> = [
|
|
568
|
+
// & must be first: other entities contain & which would get double-expanded
|
|
569
|
+
['&', '&'],
|
|
570
|
+
[' ', ' '],
|
|
571
|
+
[' ', ' '],
|
|
572
|
+
['<', '<'],
|
|
573
|
+
['>', '>'],
|
|
574
|
+
['"', '"'],
|
|
575
|
+
["'", '''],
|
|
576
|
+
["'", '''],
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
let pattern = escapeRegExp(decodedText)
|
|
580
|
+
for (const [char, entity] of entityMap) {
|
|
581
|
+
const escapedChar = escapeRegExp(char)
|
|
582
|
+
const escapedEntity = escapeRegExp(entity)
|
|
583
|
+
pattern = pattern.replace(new RegExp(escapedChar, 'g'), `(?:${escapedChar}|${escapedEntity})`)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const regex = new RegExp(pattern)
|
|
587
|
+
const match = snippet.match(regex)
|
|
588
|
+
if (match) return match[0]
|
|
589
|
+
|
|
590
|
+
// Try matching with <br> tags stripped from snippet
|
|
591
|
+
const chars = [...decodedText].map((ch) => escapeRegExp(ch))
|
|
592
|
+
const brAwarePattern = chars.join('(?:<br\\s*\\/?>)*')
|
|
593
|
+
const brRegex = new RegExp(brAwarePattern)
|
|
594
|
+
const brMatch = snippet.match(brRegex)
|
|
595
|
+
|
|
596
|
+
return brMatch && brMatch[0] !== decodedText ? brMatch[0] : null
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Resolve CMS placeholders like {{cms:cms-96}} in text.
|
|
601
|
+
*/
|
|
602
|
+
function resolveCmsPlaceholders(text: string, manifest: CmsManifest): string {
|
|
603
|
+
const placeholderPattern = /\{\{cms:([^}]+)\}\}/g
|
|
604
|
+
|
|
605
|
+
return text.replace(placeholderPattern, (match, cmsId: string) => {
|
|
606
|
+
const childEntry: ManifestEntry | undefined = manifest.entries[cmsId]
|
|
607
|
+
if (!childEntry) {
|
|
608
|
+
return match
|
|
609
|
+
}
|
|
610
|
+
if (childEntry.sourceSnippet) {
|
|
611
|
+
return childEntry.sourceSnippet
|
|
612
|
+
}
|
|
613
|
+
return childEntry.html ?? childEntry.text ?? match
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Find a src attribute with expression value (e.g., src={variable}) in text.
|
|
619
|
+
* Handles balanced braces for nested expressions.
|
|
620
|
+
* Returns the match with index and length, or null if not found.
|
|
621
|
+
*/
|
|
622
|
+
function findExpressionSrcAttribute(text: string): { index: number; length: number } | null {
|
|
623
|
+
// Find 'src=' followed by '{'
|
|
624
|
+
const srcExprStart = /src\s*=\s*\{/
|
|
625
|
+
const match = text.match(srcExprStart)
|
|
626
|
+
if (!match || match.index === undefined) return null
|
|
627
|
+
|
|
628
|
+
// Find the matching closing brace (handle nesting)
|
|
629
|
+
const braceStart = match.index + match[0].length - 1 // index of '{'
|
|
630
|
+
let depth = 1
|
|
631
|
+
let i = braceStart + 1
|
|
632
|
+
while (i < text.length && depth > 0) {
|
|
633
|
+
if (text[i] === '{') depth++
|
|
634
|
+
else if (text[i] === '}') depth--
|
|
635
|
+
i++
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (depth !== 0) return null // Unbalanced braces
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
index: match.index,
|
|
642
|
+
length: i - match.index,
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function escapeRegExp(string: string): string {
|
|
647
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
648
|
+
}
|
|
649
|
+
|