@nuasite/cms 0.7.3 → 0.8.2
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 +2 -6
- package/dist/editor.js +8983 -8467
- package/package.json +1 -1
- package/src/color-patterns.ts +68 -1
- package/src/editor/color-utils.ts +8 -8
- package/src/editor/components/bg-image-overlay.tsx +483 -0
- package/src/editor/components/editable-highlights.tsx +14 -0
- package/src/editor/components/frontmatter-fields.tsx +7 -3
- package/src/editor/components/outline.tsx +3 -2
- package/src/editor/components/text-style-toolbar.tsx +4 -1
- package/src/editor/constants.ts +3 -0
- package/src/editor/editor.ts +173 -7
- package/src/editor/history.ts +120 -34
- package/src/editor/hooks/index.ts +3 -0
- package/src/editor/hooks/useBgImageHoverDetection.ts +101 -0
- package/src/editor/index.tsx +12 -0
- package/src/editor/signals.ts +48 -14
- package/src/editor/storage.ts +50 -0
- package/src/editor/types.ts +79 -8
- package/src/handlers/source-writer.ts +61 -69
- package/src/html-processor.ts +75 -5
- package/src/source-finder/snippet-utils.ts +6 -2
- package/src/tailwind-colors.ts +1 -0
- package/src/types.ts +16 -0
package/src/editor/signals.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
MediaItem,
|
|
28
28
|
MediaLibraryState,
|
|
29
29
|
PendingAttributeChange,
|
|
30
|
+
PendingBackgroundImageChange,
|
|
30
31
|
PendingChange,
|
|
31
32
|
PendingColorChange,
|
|
32
33
|
PendingComponentInsert,
|
|
@@ -231,6 +232,9 @@ export const pendingImageChanges = signal<Map<string, PendingImageChange>>(
|
|
|
231
232
|
export const pendingColorChanges = signal<Map<string, PendingColorChange>>(
|
|
232
233
|
new Map(),
|
|
233
234
|
)
|
|
235
|
+
export const pendingBgImageChanges = signal<Map<string, PendingBackgroundImageChange>>(
|
|
236
|
+
new Map(),
|
|
237
|
+
)
|
|
234
238
|
export const manifest = signal<CmsManifest>({
|
|
235
239
|
entries: {},
|
|
236
240
|
components: {},
|
|
@@ -252,6 +256,7 @@ const _pendingComponentChangesHelpers = createMapHelpers(pendingComponentChanges
|
|
|
252
256
|
const _pendingInsertsHelpers = createMapHelpers(pendingInserts)
|
|
253
257
|
const _pendingImageChangesHelpers = createMapHelpers(pendingImageChanges)
|
|
254
258
|
const _pendingColorChangesHelpers = createMapHelpers(pendingColorChanges)
|
|
259
|
+
const _pendingBgImageChangesHelpers = createMapHelpers(pendingBgImageChanges)
|
|
255
260
|
|
|
256
261
|
// ============================================================================
|
|
257
262
|
// AI State Signals
|
|
@@ -491,13 +496,37 @@ let toastIdCounter = 0
|
|
|
491
496
|
// Computed Values - Dirty Tracking
|
|
492
497
|
// ============================================================================
|
|
493
498
|
|
|
499
|
+
/** All change categories that participate in dirty tracking.
|
|
500
|
+
* Adding a new category forces a TypeScript error until it's added to changeRegistry. */
|
|
501
|
+
type ChangeCategory = 'text' | 'image' | 'color' | 'bgImage' | 'seo' | 'attribute'
|
|
502
|
+
|
|
503
|
+
interface ChangeRegistryEntry {
|
|
504
|
+
mapSignal: Signal<Map<string, any>>
|
|
505
|
+
dirtyCount: ReturnType<typeof computed<number>>
|
|
506
|
+
hasDirty: ReturnType<typeof computed<boolean>>
|
|
507
|
+
}
|
|
508
|
+
|
|
494
509
|
// Use factory for dirty tracking to reduce duplication
|
|
495
510
|
const _pendingChangesDirty = createDirtyTracking(pendingChanges)
|
|
496
511
|
const _pendingImageChangesDirty = createDirtyTracking(pendingImageChanges)
|
|
497
512
|
const _pendingColorChangesDirty = createDirtyTracking(pendingColorChanges)
|
|
513
|
+
const _pendingBgImageChangesDirty = createDirtyTracking(pendingBgImageChanges)
|
|
498
514
|
const _pendingSeoChangesDirty = createDirtyTracking(pendingSeoChanges)
|
|
499
515
|
const _pendingAttributeChangesDirty = createDirtyTracking(pendingAttributeChanges)
|
|
500
516
|
|
|
517
|
+
export const changeRegistry: Record<ChangeCategory, ChangeRegistryEntry> = {
|
|
518
|
+
text: { mapSignal: pendingChanges, dirtyCount: _pendingChangesDirty.dirtyCount, hasDirty: _pendingChangesDirty.hasDirty },
|
|
519
|
+
image: { mapSignal: pendingImageChanges, dirtyCount: _pendingImageChangesDirty.dirtyCount, hasDirty: _pendingImageChangesDirty.hasDirty },
|
|
520
|
+
color: { mapSignal: pendingColorChanges, dirtyCount: _pendingColorChangesDirty.dirtyCount, hasDirty: _pendingColorChangesDirty.hasDirty },
|
|
521
|
+
bgImage: { mapSignal: pendingBgImageChanges, dirtyCount: _pendingBgImageChangesDirty.dirtyCount, hasDirty: _pendingBgImageChangesDirty.hasDirty },
|
|
522
|
+
seo: { mapSignal: pendingSeoChanges, dirtyCount: _pendingSeoChangesDirty.dirtyCount, hasDirty: _pendingSeoChangesDirty.hasDirty },
|
|
523
|
+
attribute: {
|
|
524
|
+
mapSignal: pendingAttributeChanges,
|
|
525
|
+
dirtyCount: _pendingAttributeChangesDirty.dirtyCount,
|
|
526
|
+
hasDirty: _pendingAttributeChangesDirty.hasDirty,
|
|
527
|
+
},
|
|
528
|
+
}
|
|
529
|
+
|
|
501
530
|
export const dirtyChangesCount = _pendingChangesDirty.dirtyCount
|
|
502
531
|
export const dirtyChanges = _pendingChangesDirty.dirtyItems
|
|
503
532
|
export const hasDirtyChanges = _pendingChangesDirty.hasDirty
|
|
@@ -510,6 +539,10 @@ export const dirtyColorChangesCount = _pendingColorChangesDirty.dirtyCount
|
|
|
510
539
|
export const dirtyColorChanges = _pendingColorChangesDirty.dirtyItems
|
|
511
540
|
export const hasDirtyColorChanges = _pendingColorChangesDirty.hasDirty
|
|
512
541
|
|
|
542
|
+
export const dirtyBgImageChangesCount = _pendingBgImageChangesDirty.dirtyCount
|
|
543
|
+
export const dirtyBgImageChanges = _pendingBgImageChangesDirty.dirtyItems
|
|
544
|
+
export const hasDirtyBgImageChanges = _pendingBgImageChangesDirty.hasDirty
|
|
545
|
+
|
|
513
546
|
export const dirtySeoChangesCount = _pendingSeoChangesDirty.dirtyCount
|
|
514
547
|
export const dirtySeoChanges = _pendingSeoChangesDirty.dirtyItems
|
|
515
548
|
export const hasDirtySeoChanges = _pendingSeoChangesDirty.hasDirty
|
|
@@ -518,16 +551,11 @@ export const dirtyAttributeChangesCount = _pendingAttributeChangesDirty.dirtyCou
|
|
|
518
551
|
export const dirtyAttributeChanges = _pendingAttributeChangesDirty.dirtyItems
|
|
519
552
|
export const hasDirtyAttributeChanges = _pendingAttributeChangesDirty.hasDirty
|
|
520
553
|
|
|
521
|
-
|
|
522
|
-
() =>
|
|
523
|
-
dirtyChangesCount.value + dirtyImageChangesCount.value + dirtyColorChangesCount.value + dirtySeoChangesCount.value
|
|
524
|
-
+ dirtyAttributeChangesCount.value,
|
|
525
|
-
)
|
|
554
|
+
const _registryEntries = Object.values(changeRegistry)
|
|
526
555
|
|
|
527
|
-
export const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
)
|
|
556
|
+
export const totalDirtyCount = computed(() => _registryEntries.reduce((sum, entry) => sum + entry.dirtyCount.value, 0))
|
|
557
|
+
|
|
558
|
+
export const hasAnyDirtyChanges = computed(() => _registryEntries.some((entry) => entry.hasDirty.value))
|
|
531
559
|
|
|
532
560
|
// Navigation index for cycling through dirty elements
|
|
533
561
|
export const changeNavigationIndex = signal<number>(0)
|
|
@@ -615,6 +643,13 @@ export const deletePendingColorChange = _pendingColorChangesHelpers.delete
|
|
|
615
643
|
export const clearPendingColorChanges = _pendingColorChangesHelpers.clear
|
|
616
644
|
export const getPendingColorChange = _pendingColorChangesHelpers.get
|
|
617
645
|
|
|
646
|
+
// Background image changes mutations - using helpers
|
|
647
|
+
export const setPendingBgImageChange = _pendingBgImageChangesHelpers.set
|
|
648
|
+
export const updatePendingBgImageChange = _pendingBgImageChangesHelpers.update
|
|
649
|
+
export const deletePendingBgImageChange = _pendingBgImageChangesHelpers.delete
|
|
650
|
+
export const clearPendingBgImageChanges = _pendingBgImageChangesHelpers.clear
|
|
651
|
+
export const getPendingBgImageChange = _pendingBgImageChangesHelpers.get
|
|
652
|
+
|
|
618
653
|
// SEO changes mutations - using helpers
|
|
619
654
|
export const setPendingSeoChange = _pendingSeoChangesHelpers.set
|
|
620
655
|
export const updatePendingSeoChange = _pendingSeoChangesHelpers.update
|
|
@@ -1327,13 +1362,12 @@ export function resetAllState(): void {
|
|
|
1327
1362
|
showingOriginal.value = false
|
|
1328
1363
|
currentEditingId.value = null
|
|
1329
1364
|
currentComponentId.value = null
|
|
1330
|
-
|
|
1365
|
+
for (const entry of Object.values(changeRegistry)) {
|
|
1366
|
+
entry.mapSignal.value = new Map()
|
|
1367
|
+
}
|
|
1368
|
+
// Non-dirty-tracked maps still cleared individually
|
|
1331
1369
|
pendingComponentChanges.value = new Map()
|
|
1332
1370
|
pendingInserts.value = new Map()
|
|
1333
|
-
pendingImageChanges.value = new Map()
|
|
1334
|
-
pendingColorChanges.value = new Map()
|
|
1335
|
-
pendingSeoChanges.value = new Map()
|
|
1336
|
-
pendingAttributeChanges.value = new Map()
|
|
1337
1371
|
manifest.value = { entries: {}, components: {}, componentDefinitions: {} }
|
|
1338
1372
|
aiState.value = createInitialAIState()
|
|
1339
1373
|
blockEditorState.value = createInitialBlockEditorState()
|
package/src/editor/storage.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { STORAGE_KEYS } from './constants'
|
|
|
2
2
|
import type {
|
|
3
3
|
CmsSettings,
|
|
4
4
|
PendingAttributeChange,
|
|
5
|
+
PendingBackgroundImageChange,
|
|
5
6
|
PendingChange,
|
|
6
7
|
PendingColorChange,
|
|
7
8
|
PendingImageChange,
|
|
8
9
|
SavedAttributeEdits,
|
|
10
|
+
SavedBackgroundImageEdits,
|
|
9
11
|
SavedColorEdits,
|
|
10
12
|
SavedEdits,
|
|
11
13
|
SavedImageEdits,
|
|
@@ -180,6 +182,53 @@ export function clearAttributeEditsFromStorage(): void {
|
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Background Image Edits
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
export function saveBgImageEditsToStorage(pendingBgImageChanges: Map<string, PendingBackgroundImageChange>): void {
|
|
190
|
+
const edits: SavedBackgroundImageEdits = {}
|
|
191
|
+
|
|
192
|
+
pendingBgImageChanges.forEach((change, cmsId) => {
|
|
193
|
+
if (change.isDirty) {
|
|
194
|
+
edits[cmsId] = {
|
|
195
|
+
originalBgImageClass: change.originalBgImageClass,
|
|
196
|
+
newBgImageClass: change.newBgImageClass,
|
|
197
|
+
originalBgSize: change.originalBgSize,
|
|
198
|
+
newBgSize: change.newBgSize,
|
|
199
|
+
originalBgPosition: change.originalBgPosition,
|
|
200
|
+
newBgPosition: change.newBgPosition,
|
|
201
|
+
originalBgRepeat: change.originalBgRepeat,
|
|
202
|
+
newBgRepeat: change.newBgRepeat,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
sessionStorage.setItem(STORAGE_KEYS.PENDING_BG_IMAGE_EDITS, JSON.stringify(edits))
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.warn('[CMS] Failed to save bg image edits to storage:', e)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function loadBgImageEditsFromStorage(): SavedBackgroundImageEdits {
|
|
215
|
+
try {
|
|
216
|
+
const stored = sessionStorage.getItem(STORAGE_KEYS.PENDING_BG_IMAGE_EDITS)
|
|
217
|
+
return stored ? JSON.parse(stored) : {}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.warn('[CMS] Failed to load bg image edits from storage:', e)
|
|
220
|
+
return {}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function clearBgImageEditsFromStorage(): void {
|
|
225
|
+
try {
|
|
226
|
+
sessionStorage.removeItem(STORAGE_KEYS.PENDING_BG_IMAGE_EDITS)
|
|
227
|
+
} catch (e) {
|
|
228
|
+
console.warn('[CMS] Failed to clear bg image edits from storage:', e)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
183
232
|
// ============================================================================
|
|
184
233
|
// Settings
|
|
185
234
|
// ============================================================================
|
|
@@ -287,4 +336,5 @@ export function clearAllEditsFromStorage(): void {
|
|
|
287
336
|
clearImageEditsFromStorage()
|
|
288
337
|
clearColorEditsFromStorage()
|
|
289
338
|
clearAttributeEditsFromStorage()
|
|
339
|
+
clearBgImageEditsFromStorage()
|
|
290
340
|
}
|
package/src/editor/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type {
|
|
|
5
5
|
Attribute,
|
|
6
6
|
AvailableColors,
|
|
7
7
|
AvailableTextStyles,
|
|
8
|
+
BackgroundImageMetadata,
|
|
8
9
|
CanonicalUrl,
|
|
9
10
|
CmsManifest,
|
|
10
11
|
CollectionDefinition,
|
|
@@ -94,6 +95,28 @@ export interface PendingColorChange {
|
|
|
94
95
|
isDirty: boolean
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
export interface PendingBackgroundImageChange {
|
|
99
|
+
element: HTMLElement
|
|
100
|
+
cmsId: string
|
|
101
|
+
/** Original bg-[url()] class */
|
|
102
|
+
originalBgImageClass: string
|
|
103
|
+
/** Current bg-[url()] class */
|
|
104
|
+
newBgImageClass: string
|
|
105
|
+
/** Original bg-size class (or empty) */
|
|
106
|
+
originalBgSize: string
|
|
107
|
+
/** Current bg-size class */
|
|
108
|
+
newBgSize: string
|
|
109
|
+
/** Original bg-position class (or empty) */
|
|
110
|
+
originalBgPosition: string
|
|
111
|
+
/** Current bg-position class */
|
|
112
|
+
newBgPosition: string
|
|
113
|
+
/** Original bg-repeat class (or empty) */
|
|
114
|
+
originalBgRepeat: string
|
|
115
|
+
/** Current bg-repeat class */
|
|
116
|
+
newBgRepeat: string
|
|
117
|
+
isDirty: boolean
|
|
118
|
+
}
|
|
119
|
+
|
|
97
120
|
export interface ColorEditorState {
|
|
98
121
|
isOpen: boolean
|
|
99
122
|
targetElementId: string | null
|
|
@@ -132,14 +155,42 @@ export interface SavedColorEdits {
|
|
|
132
155
|
[cmsId: string]: SavedColorEdit
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
158
|
+
export interface SavedBackgroundImageEdit {
|
|
159
|
+
originalBgImageClass: string
|
|
160
|
+
newBgImageClass: string
|
|
161
|
+
originalBgSize: string
|
|
162
|
+
newBgSize: string
|
|
163
|
+
originalBgPosition: string
|
|
164
|
+
newBgPosition: string
|
|
165
|
+
originalBgRepeat: string
|
|
166
|
+
newBgRepeat: string
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface SavedBackgroundImageEdits {
|
|
170
|
+
[cmsId: string]: SavedBackgroundImageEdit
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Style change details for updating element classes (colors, text styles, bg images) */
|
|
174
|
+
export interface StyleChangePayload {
|
|
175
|
+
/** The class to replace (e.g., 'bg-blue-500') */
|
|
138
176
|
oldClass: string
|
|
139
|
-
/** The new
|
|
177
|
+
/** The new class (e.g., 'bg-red-500') */
|
|
140
178
|
newClass: string
|
|
141
|
-
/** Type of
|
|
142
|
-
type:
|
|
179
|
+
/** Type of style change */
|
|
180
|
+
type:
|
|
181
|
+
| 'bg'
|
|
182
|
+
| 'text'
|
|
183
|
+
| 'border'
|
|
184
|
+
| 'hoverBg'
|
|
185
|
+
| 'hoverText'
|
|
186
|
+
| 'fontWeight'
|
|
187
|
+
| 'fontStyle'
|
|
188
|
+
| 'textDecoration'
|
|
189
|
+
| 'fontSize'
|
|
190
|
+
| 'bgImage'
|
|
191
|
+
| 'bgSize'
|
|
192
|
+
| 'bgPosition'
|
|
193
|
+
| 'bgRepeat'
|
|
143
194
|
/** Path to the source file where the color class is defined */
|
|
144
195
|
sourcePath?: string
|
|
145
196
|
/** Line number where the color class is defined */
|
|
@@ -169,8 +220,8 @@ export interface ChangePayload {
|
|
|
169
220
|
newSrc: string
|
|
170
221
|
newAlt?: string
|
|
171
222
|
}
|
|
172
|
-
/**
|
|
173
|
-
|
|
223
|
+
/** Style class change (colors, text styles, bg images) */
|
|
224
|
+
styleChange?: StyleChangePayload
|
|
174
225
|
/** Attribute changes (for links, forms, etc.) */
|
|
175
226
|
attributeChanges?: AttributeChangePayload[]
|
|
176
227
|
}
|
|
@@ -579,12 +630,32 @@ export interface UndoSeoAction {
|
|
|
579
630
|
wasDirty: boolean
|
|
580
631
|
}
|
|
581
632
|
|
|
633
|
+
export interface UndoBgImageAction {
|
|
634
|
+
type: 'bgImage'
|
|
635
|
+
cmsId: string
|
|
636
|
+
element: HTMLElement
|
|
637
|
+
previousClassName: string
|
|
638
|
+
currentClassName: string
|
|
639
|
+
previousStyleCssText: string
|
|
640
|
+
currentStyleCssText: string
|
|
641
|
+
previousBgImageClass: string
|
|
642
|
+
currentBgImageClass: string
|
|
643
|
+
previousBgSize: string
|
|
644
|
+
currentBgSize: string
|
|
645
|
+
previousBgPosition: string
|
|
646
|
+
currentBgPosition: string
|
|
647
|
+
previousBgRepeat: string
|
|
648
|
+
currentBgRepeat: string
|
|
649
|
+
wasDirty: boolean
|
|
650
|
+
}
|
|
651
|
+
|
|
582
652
|
export type UndoAction =
|
|
583
653
|
| UndoTextAction
|
|
584
654
|
| UndoImageAction
|
|
585
655
|
| UndoColorAction
|
|
586
656
|
| UndoAttributeAction
|
|
587
657
|
| UndoSeoAction
|
|
658
|
+
| UndoBgImageAction
|
|
588
659
|
|
|
589
660
|
declare global {
|
|
590
661
|
interface Window {
|
|
@@ -1,53 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { getProjectRoot } from '../config'
|
|
4
|
+
import type { AttributeChangePayload, ChangePayload, SaveBatchRequest } from '../editor/types'
|
|
4
5
|
import type { ManifestWriter } from '../manifest-writer'
|
|
5
6
|
import type { CmsManifest, ManifestEntry } from '../types'
|
|
6
7
|
import { acquireFileLock, escapeReplacement, normalizePagePath, resolveAndValidatePath } from '../utils'
|
|
7
8
|
|
|
8
|
-
export interface ColorChangePayload {
|
|
9
|
-
oldClass: string
|
|
10
|
-
newClass: string
|
|
11
|
-
type: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText' | 'fontWeight' | 'fontStyle' | 'textDecoration' | 'fontSize'
|
|
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
9
|
export interface SaveBatchResponse {
|
|
52
10
|
updated: number
|
|
53
11
|
errors?: Array<{ cmsId: string; error: string }>
|
|
@@ -157,8 +115,8 @@ function applyChanges(
|
|
|
157
115
|
continue
|
|
158
116
|
}
|
|
159
117
|
|
|
160
|
-
// Handle
|
|
161
|
-
if (change.
|
|
118
|
+
// Handle style class changes (colors, text styles, bg images)
|
|
119
|
+
if (change.styleChange) {
|
|
162
120
|
const result = applyColorChange(newContent, change)
|
|
163
121
|
if (result.success) {
|
|
164
122
|
newContent = result.content
|
|
@@ -323,10 +281,10 @@ function applyColorChange(
|
|
|
323
281
|
content: string,
|
|
324
282
|
change: ChangePayload,
|
|
325
283
|
): { success: true; content: string } | { success: false; error: string } {
|
|
326
|
-
const { oldClass, newClass } = change.
|
|
327
|
-
// Prefer
|
|
284
|
+
const { oldClass, newClass } = change.styleChange!
|
|
285
|
+
// Prefer styleChange's own sourceLine (points to the class attribute)
|
|
328
286
|
// over the outer change.sourceLine (may point to a data declaration)
|
|
329
|
-
const sourceLine = change.
|
|
287
|
+
const sourceLine = change.styleChange!.sourceLine ?? change.sourceLine
|
|
330
288
|
|
|
331
289
|
// When oldClass is empty, we're adding a new color class (not replacing)
|
|
332
290
|
if (!oldClass) {
|
|
@@ -346,10 +304,12 @@ function replaceClassInAttribute(
|
|
|
346
304
|
newClass: string,
|
|
347
305
|
sourceLine?: number,
|
|
348
306
|
): { success: true; content: string } | { success: false; error: string } {
|
|
349
|
-
const classAttrPattern = /(class\s*=\s*)(["'])([^"']*)\2/
|
|
350
|
-
|
|
351
307
|
const replaceOnLine = (line: string): string | null => {
|
|
352
|
-
|
|
308
|
+
// Build pattern dynamically to only exclude the actual quote character used,
|
|
309
|
+
// so bg-[url('/path')] works inside class="..." (single quotes allowed in double-quoted attr)
|
|
310
|
+
const dqMatch = line.match(/(class\s*=\s*)(")([^"]*)"/)
|
|
311
|
+
const sqMatch = line.match(/(class\s*=\s*)(')([^']*)'/)
|
|
312
|
+
const match = dqMatch || sqMatch
|
|
353
313
|
if (!match) return null
|
|
354
314
|
|
|
355
315
|
const prefix = match[1]!
|
|
@@ -365,7 +325,7 @@ function replaceClassInAttribute(
|
|
|
365
325
|
} else {
|
|
366
326
|
classes.splice(idx, 1)
|
|
367
327
|
}
|
|
368
|
-
return line.replace(
|
|
328
|
+
return line.replace(match[0], `${prefix}${quote}${classes.join(' ')}${quote}`)
|
|
369
329
|
}
|
|
370
330
|
|
|
371
331
|
if (sourceLine) {
|
|
@@ -403,12 +363,23 @@ function appendClassToAttribute(
|
|
|
403
363
|
newClass: string,
|
|
404
364
|
sourceLine?: number,
|
|
405
365
|
): { success: true; content: string } | { success: false; error: string } {
|
|
406
|
-
|
|
366
|
+
// Match class attribute with either quote, only excluding the actual quote used
|
|
367
|
+
// so bg-[url('/path')] works inside class="..."
|
|
368
|
+
const matchClassAttr = (line: string) => {
|
|
369
|
+
return line.match(/(class\s*=\s*")(([^"]*))(")/)
|
|
370
|
+
|| line.match(/(class\s*=\s*')(([^']*))(')/)
|
|
371
|
+
}
|
|
407
372
|
|
|
408
|
-
const
|
|
373
|
+
const doAppendOnLine = (line: string): string | null => {
|
|
374
|
+
const match = matchClassAttr(line)
|
|
375
|
+
if (!match) return null
|
|
376
|
+
const open = match[1]!
|
|
377
|
+
const classes = match[2]!
|
|
378
|
+
const close = match[4]!
|
|
409
379
|
const trimmed = classes.trimEnd()
|
|
410
380
|
const separator = trimmed ? ' ' : ''
|
|
411
|
-
|
|
381
|
+
const replacement = `${open}${trimmed}${separator}${escapeReplacement(newClass)}${close}`
|
|
382
|
+
return line.replace(match[0], replacement)
|
|
412
383
|
}
|
|
413
384
|
|
|
414
385
|
if (sourceLine) {
|
|
@@ -416,9 +387,9 @@ function appendClassToAttribute(
|
|
|
416
387
|
const lineIndex = sourceLine - 1
|
|
417
388
|
|
|
418
389
|
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
421
|
-
lines[lineIndex] =
|
|
390
|
+
const result = doAppendOnLine(lines[lineIndex]!)
|
|
391
|
+
if (result !== null) {
|
|
392
|
+
lines[lineIndex] = result
|
|
422
393
|
return { success: true, content: lines.join('\n') }
|
|
423
394
|
}
|
|
424
395
|
return { success: false, error: `No class attribute found on line ${sourceLine}` }
|
|
@@ -426,10 +397,13 @@ function appendClassToAttribute(
|
|
|
426
397
|
return { success: false, error: `Invalid source line ${sourceLine}` }
|
|
427
398
|
}
|
|
428
399
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
400
|
+
// Fallback: find the first class attribute in the content
|
|
401
|
+
const lines = content.split('\n')
|
|
402
|
+
for (let i = 0; i < lines.length; i++) {
|
|
403
|
+
const result = doAppendOnLine(lines[i]!)
|
|
404
|
+
if (result !== null) {
|
|
405
|
+
lines[i] = result
|
|
406
|
+
return { success: true, content: lines.join('\n') }
|
|
433
407
|
}
|
|
434
408
|
}
|
|
435
409
|
return { success: false, error: 'No class attribute found in source file' }
|
|
@@ -520,7 +494,7 @@ function applyAttributeChanges(
|
|
|
520
494
|
return { content: newContent, appliedCount: attrApplied, failedChanges }
|
|
521
495
|
}
|
|
522
496
|
|
|
523
|
-
function applyTextChange(
|
|
497
|
+
export function applyTextChange(
|
|
524
498
|
content: string,
|
|
525
499
|
change: ChangePayload,
|
|
526
500
|
manifest: CmsManifest,
|
|
@@ -530,7 +504,14 @@ function applyTextChange(
|
|
|
530
504
|
let newText = htmlValue ?? newValue
|
|
531
505
|
newText = resolveCmsPlaceholders(newText, manifest)
|
|
532
506
|
|
|
533
|
-
|
|
507
|
+
// Resolve CMS placeholders in originalValue too — when a parent element has
|
|
508
|
+
// child CMS elements, originalValue contains {{cms:cms-N}} placeholders but
|
|
509
|
+
// the sourceSnippet contains the actual HTML for those children.
|
|
510
|
+
const resolvedOriginal = originalValue
|
|
511
|
+
? resolveCmsPlaceholders(originalValue, manifest)
|
|
512
|
+
: originalValue
|
|
513
|
+
|
|
514
|
+
if (!sourceSnippet || !resolvedOriginal) {
|
|
534
515
|
if (change.attributeChanges && change.attributeChanges.length > 0) {
|
|
535
516
|
return { success: true, content }
|
|
536
517
|
}
|
|
@@ -541,19 +522,30 @@ function applyTextChange(
|
|
|
541
522
|
return { success: false, error: 'Source snippet not found in file' }
|
|
542
523
|
}
|
|
543
524
|
|
|
544
|
-
// Replace
|
|
545
|
-
const updatedSnippet = sourceSnippet.replace(
|
|
525
|
+
// Replace resolvedOriginal with newText WITHIN the sourceSnippet
|
|
526
|
+
const updatedSnippet = sourceSnippet.replace(resolvedOriginal, newText)
|
|
546
527
|
|
|
547
528
|
if (updatedSnippet === sourceSnippet) {
|
|
548
|
-
//
|
|
549
|
-
const matchedText = findTextInSnippet(sourceSnippet,
|
|
529
|
+
// resolvedOriginal wasn't found in snippet - try HTML entity handling
|
|
530
|
+
const matchedText = findTextInSnippet(sourceSnippet, resolvedOriginal)
|
|
550
531
|
if (matchedText) {
|
|
551
532
|
const updatedWithEntity = sourceSnippet.replace(matchedText, newText)
|
|
552
533
|
return { success: true, content: content.replace(sourceSnippet, updatedWithEntity) }
|
|
553
534
|
}
|
|
535
|
+
// Try inner content replacement for text spanning inline HTML elements
|
|
536
|
+
// (e.g., <h3>text part 1 <span class="...">text part 2</span></h3>)
|
|
537
|
+
const innerMatch = sourceSnippet.match(/^(\s*<(\w+)\b[^>]*>)([\s\S]*)(<\/\2>\s*)$/)
|
|
538
|
+
if (innerMatch) {
|
|
539
|
+
const [, openTag, , innerContent, closeTag] = innerMatch
|
|
540
|
+
const textOnly = innerContent!.replace(/<[^>]+>/g, '')
|
|
541
|
+
if (textOnly === resolvedOriginal) {
|
|
542
|
+
return { success: true, content: content.replace(sourceSnippet, openTag + newText + closeTag) }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
554
546
|
return {
|
|
555
547
|
success: false,
|
|
556
|
-
error: `Original text "${
|
|
548
|
+
error: `Original text "${resolvedOriginal.substring(0, 50)}..." not found in source snippet`,
|
|
557
549
|
}
|
|
558
550
|
}
|
|
559
551
|
|
package/src/html-processor.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type HTMLElement as ParsedHTMLElement, parse } from 'node-html-parser'
|
|
2
2
|
import { processSeoFromHtml } from './seo-processor'
|
|
3
3
|
import { enhanceManifestWithSourceSnippets } from './source-finder'
|
|
4
|
-
import { extractColorClasses, extractTextStyleClasses } from './tailwind-colors'
|
|
5
|
-
import type { Attribute, ComponentInstance, ImageMetadata, ManifestEntry, PageSeoData, SeoOptions } from './types'
|
|
4
|
+
import { extractBackgroundImageClasses, extractColorClasses, extractTextStyleClasses } from './tailwind-colors'
|
|
5
|
+
import type { Attribute, BackgroundImageMetadata, ComponentInstance, ImageMetadata, ManifestEntry, PageSeoData, SeoOptions } from './types'
|
|
6
6
|
import { generateStableId } from './utils'
|
|
7
7
|
|
|
8
8
|
/** Type for parsed HTML element nodes from node-html-parser */
|
|
@@ -612,6 +612,68 @@ export async function processHtml(
|
|
|
612
612
|
})
|
|
613
613
|
})
|
|
614
614
|
|
|
615
|
+
// Background image detection pass: mark elements with bg-[url()] classes
|
|
616
|
+
interface BgImageEntry {
|
|
617
|
+
metadata: BackgroundImageMetadata
|
|
618
|
+
sourceFile?: string
|
|
619
|
+
sourceLine?: number
|
|
620
|
+
}
|
|
621
|
+
const bgImageEntries = new Map<string, BgImageEntry>()
|
|
622
|
+
root.querySelectorAll('*').forEach((node) => {
|
|
623
|
+
// Skip already-marked elements
|
|
624
|
+
if (node.getAttribute(attributeName)) return
|
|
625
|
+
|
|
626
|
+
// Skip elements inside markdown wrapper
|
|
627
|
+
if (isInsideMarkdownWrapper(node)) return
|
|
628
|
+
|
|
629
|
+
const classAttr = node.getAttribute('class')
|
|
630
|
+
const bgMeta = extractBackgroundImageClasses(classAttr)
|
|
631
|
+
if (!bgMeta) return
|
|
632
|
+
|
|
633
|
+
// When skipMarkdownContent is true, only mark elements with source file attributes
|
|
634
|
+
if (skipMarkdownContent) {
|
|
635
|
+
let hasSourceAttr = false
|
|
636
|
+
let current: HTMLNode | null = node
|
|
637
|
+
while (current) {
|
|
638
|
+
if (current.getAttribute?.('data-astro-source-file')) {
|
|
639
|
+
hasSourceAttr = true
|
|
640
|
+
break
|
|
641
|
+
}
|
|
642
|
+
current = current.parentNode as HTMLNode | null
|
|
643
|
+
}
|
|
644
|
+
if (!hasSourceAttr) return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const id = getNextId()
|
|
648
|
+
node.setAttribute(attributeName, id)
|
|
649
|
+
node.setAttribute('data-cms-bg-img', 'true')
|
|
650
|
+
|
|
651
|
+
// Try to get source location from the element itself or ancestors
|
|
652
|
+
let sourceFile: string | undefined
|
|
653
|
+
let sourceLine: number | undefined
|
|
654
|
+
let current: HTMLNode | null = node
|
|
655
|
+
while (current && !sourceFile) {
|
|
656
|
+
const file = current.getAttribute?.('data-astro-source-file')
|
|
657
|
+
const line = current.getAttribute?.('data-astro-source-loc') || current.getAttribute?.('data-astro-source-line')
|
|
658
|
+
if (file) {
|
|
659
|
+
sourceFile = file
|
|
660
|
+
if (line) {
|
|
661
|
+
const lineNum = parseInt(line.split(':')[0] ?? '1', 10)
|
|
662
|
+
if (!Number.isNaN(lineNum)) {
|
|
663
|
+
sourceLine = lineNum
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
current = current.parentNode as HTMLNode | null
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
bgImageEntries.set(id, {
|
|
671
|
+
metadata: bgMeta,
|
|
672
|
+
sourceFile,
|
|
673
|
+
sourceLine,
|
|
674
|
+
})
|
|
675
|
+
})
|
|
676
|
+
|
|
615
677
|
// Third pass: collect candidate text elements (don't mark yet)
|
|
616
678
|
// We collect candidates first to filter out pure containers before marking
|
|
617
679
|
interface TextCandidate {
|
|
@@ -826,12 +888,18 @@ export async function processHtml(
|
|
|
826
888
|
const imageInfo = imageEntries.get(id)
|
|
827
889
|
const isImage = !!imageInfo
|
|
828
890
|
|
|
891
|
+
// Check if this is a background image entry
|
|
892
|
+
const bgImageInfo = bgImageEntries.get(id)
|
|
893
|
+
// Also extract bg image classes fresh for elements marked for other reasons
|
|
894
|
+
const bgImageClassAttr = node.getAttribute('class')
|
|
895
|
+
const bgImageMetadata = bgImageInfo?.metadata ?? extractBackgroundImageClasses(bgImageClassAttr)
|
|
896
|
+
|
|
829
897
|
// Check if this is the collection wrapper
|
|
830
898
|
const isCollectionWrapper = id === collectionWrapperId
|
|
831
899
|
|
|
832
900
|
const entryText = isImage ? (imageInfo.metadata.alt || imageInfo.metadata.src) : textWithPlaceholders.trim()
|
|
833
|
-
// For images, use the source file we captured from ancestors if not in sourceLocationMap
|
|
834
|
-
const entrySourcePath = sourceLocation?.file || imageInfo?.sourceFile || sourcePath
|
|
901
|
+
// For images/bg-images, use the source file we captured from ancestors if not in sourceLocationMap
|
|
902
|
+
const entrySourcePath = sourceLocation?.file || imageInfo?.sourceFile || bgImageInfo?.sourceFile || sourcePath
|
|
835
903
|
|
|
836
904
|
// Generate stable ID based on content and context
|
|
837
905
|
const stableId = generateStableId(tag, entryText, entrySourcePath)
|
|
@@ -854,7 +922,7 @@ export async function processHtml(
|
|
|
854
922
|
html: htmlContent,
|
|
855
923
|
sourcePath: entrySourcePath,
|
|
856
924
|
childCmsIds: childCmsIds.length > 0 ? childCmsIds : undefined,
|
|
857
|
-
sourceLine: sourceLocation?.line ?? imageInfo?.sourceLine,
|
|
925
|
+
sourceLine: sourceLocation?.line ?? imageInfo?.sourceLine ?? bgImageInfo?.sourceLine,
|
|
858
926
|
sourceSnippet: undefined,
|
|
859
927
|
variableName: undefined,
|
|
860
928
|
parentComponentId,
|
|
@@ -866,6 +934,8 @@ export async function processHtml(
|
|
|
866
934
|
stableId,
|
|
867
935
|
// Image metadata for image entries
|
|
868
936
|
imageMetadata: imageInfo?.metadata,
|
|
937
|
+
// Background image metadata for bg-[url()] elements
|
|
938
|
+
backgroundImage: bgImageMetadata,
|
|
869
939
|
// Color and text style classes for buttons/styled elements
|
|
870
940
|
colorClasses: allTrackedClasses,
|
|
871
941
|
// All attributes with resolved values (isStatic will be updated later from source)
|