@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.
@@ -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
- const match = line.match(classAttrPattern)
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(classAttrPattern, `${prefix}${quote}${classes.join(' ')}${quote}`)
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
- const appendPattern = /(class\s*=\s*["'])([^"']*)(["'])/
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 doAppend = (_: string, open: string, classes: string, close: string) => {
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
- return `${open}${trimmed}${separator}${escapeReplacement(newClass)}${close}`
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 line = lines[lineIndex]!
420
- if (appendPattern.test(line)) {
421
- lines[lineIndex] = line.replace(appendPattern, doAppend)
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
- if (appendPattern.test(content)) {
430
- return {
431
- success: true,
432
- content: content.replace(appendPattern, doAppend),
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' }
@@ -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
- if (classAttr && hasOnlyTextStyleClasses(classAttr)) {
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
- if (!directText && hasDescendants) {
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, propName, exprPath, entry.text!,
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), trimmedText,
688
+ path.join(srcDir, searchDir),
689
+ trimmedText,
686
690
  )
687
691
  if (result) {
688
692
  const parentSnippet = result.snippet ?? trimmedText
@@ -9,6 +9,7 @@ export {
9
9
  buildColorClass,
10
10
  COLOR_CLASS_PATTERNS,
11
11
  DEFAULT_TAILWIND_COLORS,
12
+ extractBackgroundImageClasses,
12
13
  extractColorClasses,
13
14
  extractTextStyleClasses,
14
15
  getColorType,
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.) */