@nuasite/cms 0.7.2 → 0.8.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 +1 -5
- package/dist/editor.js +8825 -8384
- 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 +456 -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 +170 -2
- 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 +21 -2
- package/src/editor/storage.ts +50 -0
- package/src/editor/types.ts +52 -1
- package/src/handlers/source-writer.ts +30 -14
- package/src/html-processor.ts +83 -7
- package/src/source-finder/snippet-utils.ts +6 -2
- package/src/tailwind-colors.ts +1 -0
- package/src/types.ts +16 -0
|
@@ -346,10 +346,12 @@ function replaceClassInAttribute(
|
|
|
346
346
|
newClass: string,
|
|
347
347
|
sourceLine?: number,
|
|
348
348
|
): { success: true; content: string } | { success: false; error: string } {
|
|
349
|
-
const classAttrPattern = /(class\s*=\s*)(["'])([^"']*)\2/
|
|
350
|
-
|
|
351
349
|
const replaceOnLine = (line: string): string | null => {
|
|
352
|
-
|
|
350
|
+
// Build pattern dynamically to only exclude the actual quote character used,
|
|
351
|
+
// so bg-[url('/path')] works inside class="..." (single quotes allowed in double-quoted attr)
|
|
352
|
+
const dqMatch = line.match(/(class\s*=\s*)(")([^"]*)"/)
|
|
353
|
+
const sqMatch = line.match(/(class\s*=\s*)(')([^']*)'/)
|
|
354
|
+
const match = dqMatch || sqMatch
|
|
353
355
|
if (!match) return null
|
|
354
356
|
|
|
355
357
|
const prefix = match[1]!
|
|
@@ -365,7 +367,7 @@ function replaceClassInAttribute(
|
|
|
365
367
|
} else {
|
|
366
368
|
classes.splice(idx, 1)
|
|
367
369
|
}
|
|
368
|
-
return line.replace(
|
|
370
|
+
return line.replace(match[0], `${prefix}${quote}${classes.join(' ')}${quote}`)
|
|
369
371
|
}
|
|
370
372
|
|
|
371
373
|
if (sourceLine) {
|
|
@@ -403,12 +405,23 @@ function appendClassToAttribute(
|
|
|
403
405
|
newClass: string,
|
|
404
406
|
sourceLine?: number,
|
|
405
407
|
): { success: true; content: string } | { success: false; error: string } {
|
|
406
|
-
|
|
408
|
+
// Match class attribute with either quote, only excluding the actual quote used
|
|
409
|
+
// so bg-[url('/path')] works inside class="..."
|
|
410
|
+
const matchClassAttr = (line: string) => {
|
|
411
|
+
return line.match(/(class\s*=\s*")(([^"]*))(")/)
|
|
412
|
+
|| line.match(/(class\s*=\s*')(([^']*))(')/)
|
|
413
|
+
}
|
|
407
414
|
|
|
408
|
-
const
|
|
415
|
+
const doAppendOnLine = (line: string): string | null => {
|
|
416
|
+
const match = matchClassAttr(line)
|
|
417
|
+
if (!match) return null
|
|
418
|
+
const open = match[1]!
|
|
419
|
+
const classes = match[2]!
|
|
420
|
+
const close = match[4]!
|
|
409
421
|
const trimmed = classes.trimEnd()
|
|
410
422
|
const separator = trimmed ? ' ' : ''
|
|
411
|
-
|
|
423
|
+
const replacement = `${open}${trimmed}${separator}${escapeReplacement(newClass)}${close}`
|
|
424
|
+
return line.replace(match[0], replacement)
|
|
412
425
|
}
|
|
413
426
|
|
|
414
427
|
if (sourceLine) {
|
|
@@ -416,9 +429,9 @@ function appendClassToAttribute(
|
|
|
416
429
|
const lineIndex = sourceLine - 1
|
|
417
430
|
|
|
418
431
|
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
421
|
-
lines[lineIndex] =
|
|
432
|
+
const result = doAppendOnLine(lines[lineIndex]!)
|
|
433
|
+
if (result !== null) {
|
|
434
|
+
lines[lineIndex] = result
|
|
422
435
|
return { success: true, content: lines.join('\n') }
|
|
423
436
|
}
|
|
424
437
|
return { success: false, error: `No class attribute found on line ${sourceLine}` }
|
|
@@ -426,10 +439,13 @@ function appendClassToAttribute(
|
|
|
426
439
|
return { success: false, error: `Invalid source line ${sourceLine}` }
|
|
427
440
|
}
|
|
428
441
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
442
|
+
// Fallback: find the first class attribute in the content
|
|
443
|
+
const lines = content.split('\n')
|
|
444
|
+
for (let i = 0; i < lines.length; i++) {
|
|
445
|
+
const result = doAppendOnLine(lines[i]!)
|
|
446
|
+
if (result !== null) {
|
|
447
|
+
lines[i] = result
|
|
448
|
+
return { success: true, content: lines.join('\n') }
|
|
433
449
|
}
|
|
434
450
|
}
|
|
435
451
|
return { success: false, error: 'No class attribute found in source file' }
|
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 {
|
|
@@ -645,7 +707,12 @@ export async function processHtml(
|
|
|
645
707
|
// Only apply when includeTags is null or doesn't include 'span'
|
|
646
708
|
if (skipInlineStyleTags && (includeTags === null || !includeTags.includes('span')) && tag === 'span') {
|
|
647
709
|
const classAttr = node.getAttribute('class')
|
|
648
|
-
|
|
710
|
+
// Skip bare spans (no classes) - they're just text wrappers
|
|
711
|
+
if (!classAttr || !classAttr.trim()) {
|
|
712
|
+
return
|
|
713
|
+
}
|
|
714
|
+
// Skip styled spans (only text styling classes)
|
|
715
|
+
if (hasOnlyTextStyleClasses(classAttr)) {
|
|
649
716
|
return
|
|
650
717
|
}
|
|
651
718
|
}
|
|
@@ -711,7 +778,8 @@ export async function processHtml(
|
|
|
711
778
|
const hasDescendants = hasCandidateDescendants(node)
|
|
712
779
|
|
|
713
780
|
// Skip pure containers - they have no direct text and all content comes from children
|
|
714
|
-
|
|
781
|
+
// Exempt <a> elements - they have editable attributes (href)
|
|
782
|
+
if (!directText && hasDescendants && candidate.tag !== 'a') {
|
|
715
783
|
candidateNodes.delete(node) // Remove from candidates so nested checks stay accurate
|
|
716
784
|
continue
|
|
717
785
|
}
|
|
@@ -820,12 +888,18 @@ export async function processHtml(
|
|
|
820
888
|
const imageInfo = imageEntries.get(id)
|
|
821
889
|
const isImage = !!imageInfo
|
|
822
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
|
+
|
|
823
897
|
// Check if this is the collection wrapper
|
|
824
898
|
const isCollectionWrapper = id === collectionWrapperId
|
|
825
899
|
|
|
826
900
|
const entryText = isImage ? (imageInfo.metadata.alt || imageInfo.metadata.src) : textWithPlaceholders.trim()
|
|
827
|
-
// For images, use the source file we captured from ancestors if not in sourceLocationMap
|
|
828
|
-
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
|
|
829
903
|
|
|
830
904
|
// Generate stable ID based on content and context
|
|
831
905
|
const stableId = generateStableId(tag, entryText, entrySourcePath)
|
|
@@ -848,7 +922,7 @@ export async function processHtml(
|
|
|
848
922
|
html: htmlContent,
|
|
849
923
|
sourcePath: entrySourcePath,
|
|
850
924
|
childCmsIds: childCmsIds.length > 0 ? childCmsIds : undefined,
|
|
851
|
-
sourceLine: sourceLocation?.line ?? imageInfo?.sourceLine,
|
|
925
|
+
sourceLine: sourceLocation?.line ?? imageInfo?.sourceLine ?? bgImageInfo?.sourceLine,
|
|
852
926
|
sourceSnippet: undefined,
|
|
853
927
|
variableName: undefined,
|
|
854
928
|
parentComponentId,
|
|
@@ -860,6 +934,8 @@ export async function processHtml(
|
|
|
860
934
|
stableId,
|
|
861
935
|
// Image metadata for image entries
|
|
862
936
|
imageMetadata: imageInfo?.metadata,
|
|
937
|
+
// Background image metadata for bg-[url()] elements
|
|
938
|
+
backgroundImage: bgImageMetadata,
|
|
863
939
|
// Color and text style classes for buttons/styled elements
|
|
864
940
|
colorClasses: allTrackedClasses,
|
|
865
941
|
// All attributes with resolved values (isStatic will be updated later from source)
|
|
@@ -656,7 +656,10 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
656
656
|
const propName = cached.propAliases.get(baseVar)!
|
|
657
657
|
const componentFileName = path.basename(filePath)
|
|
658
658
|
const result = await searchForExpressionProp(
|
|
659
|
-
componentFileName,
|
|
659
|
+
componentFileName,
|
|
660
|
+
propName,
|
|
661
|
+
exprPath,
|
|
662
|
+
entry.text!,
|
|
660
663
|
)
|
|
661
664
|
if (result) {
|
|
662
665
|
const propSnippet = result.snippet ?? trimmedText
|
|
@@ -682,7 +685,8 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
682
685
|
for (const searchDir of ['pages', 'components', 'layouts']) {
|
|
683
686
|
try {
|
|
684
687
|
const result = await searchForPropInParents(
|
|
685
|
-
path.join(srcDir, searchDir),
|
|
688
|
+
path.join(srcDir, searchDir),
|
|
689
|
+
trimmedText,
|
|
686
690
|
)
|
|
687
691
|
if (result) {
|
|
688
692
|
const parentSnippet = result.snippet ?? trimmedText
|
package/src/tailwind-colors.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -42,6 +42,20 @@ export interface ComponentDefinition {
|
|
|
42
42
|
previewWidth?: number
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** Background image metadata for elements using bg-[url()] */
|
|
46
|
+
export interface BackgroundImageMetadata {
|
|
47
|
+
/** Full Tailwind class, e.g. bg-[url('/path.png')] */
|
|
48
|
+
bgImageClass: string
|
|
49
|
+
/** Extracted image URL, e.g. /path.png */
|
|
50
|
+
imageUrl: string
|
|
51
|
+
/** Background size class: bg-auto | bg-cover | bg-contain */
|
|
52
|
+
bgSize?: string
|
|
53
|
+
/** Background position class: bg-center | bg-top | bg-bottom-left | ... */
|
|
54
|
+
bgPosition?: string
|
|
55
|
+
/** Background repeat class: bg-repeat | bg-no-repeat | bg-repeat-x | bg-repeat-y */
|
|
56
|
+
bgRepeat?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
/** Image metadata for better tracking and integrity */
|
|
46
60
|
export interface ImageMetadata {
|
|
47
61
|
/** Image source URL */
|
|
@@ -155,6 +169,8 @@ export interface ManifestEntry {
|
|
|
155
169
|
sourceHash?: string
|
|
156
170
|
/** Image metadata for img elements (replaces imageSrc/imageAlt) */
|
|
157
171
|
imageMetadata?: ImageMetadata
|
|
172
|
+
/** Background image metadata for elements using bg-[url()] */
|
|
173
|
+
backgroundImage?: BackgroundImageMetadata
|
|
158
174
|
/** Content validation constraints */
|
|
159
175
|
constraints?: ContentConstraints
|
|
160
176
|
/** Color classes applied to this element (for buttons, etc.) */
|