@nuasite/cms-marker 0.0.52 → 0.0.54
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 +0 -4
- package/dist/types/astro-transform.d.ts +21 -0
- package/dist/types/astro-transform.d.ts.map +1 -0
- package/dist/types/build-processor.d.ts +10 -0
- package/dist/types/build-processor.d.ts.map +1 -0
- package/dist/types/component-registry.d.ts +63 -0
- package/dist/types/component-registry.d.ts.map +1 -0
- package/dist/types/dev-middleware.d.ts +7 -0
- package/dist/types/dev-middleware.d.ts.map +1 -0
- package/dist/types/html-processor.d.ts +51 -0
- package/dist/types/html-processor.d.ts.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/manifest-writer.d.ts +75 -0
- package/dist/types/manifest-writer.d.ts.map +1 -0
- package/dist/types/source-finder.d.ts +97 -0
- package/dist/types/source-finder.d.ts.map +1 -0
- package/dist/types/tailwind-colors.d.ts +66 -0
- package/dist/types/tailwind-colors.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dist/types/types.d.ts +195 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/utils.d.ts +38 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/types/vite-plugin.d.ts +14 -0
- package/dist/types/vite-plugin.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/astro-transform.ts +4 -4
- package/src/build-processor.ts +1 -1
- package/src/component-registry.ts +2 -2
- package/src/dev-middleware.ts +6 -6
- package/src/html-processor.ts +180 -6
- package/src/index.ts +0 -1
- package/src/manifest-writer.ts +47 -1
- package/src/source-finder.ts +142 -9
- package/src/tailwind-colors.ts +338 -0
- package/src/tsconfig.json +1 -1
- package/src/types.ts +123 -1
- package/src/utils.ts +99 -0
package/src/source-finder.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import type { ManifestEntry } from './types'
|
|
4
|
+
import { generateSourceHash } from './utils'
|
|
3
5
|
|
|
4
6
|
export interface SourceLocation {
|
|
5
7
|
file: string
|
|
@@ -182,14 +184,18 @@ async function searchAstroFile(
|
|
|
182
184
|
const matchLength = Math.min(cleanText.length, sectionTextOnly.length)
|
|
183
185
|
score = 50 + (matchLength / cleanText.length) * 40
|
|
184
186
|
matched = true
|
|
185
|
-
|
|
187
|
+
// Find the actual line containing the text
|
|
188
|
+
const actualLine = findLineContainingText(lines, i, 5, textPreview)
|
|
189
|
+
matches.push({ line: actualLine, score, type: 'static' })
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
// Check for short exact text match (static content)
|
|
189
193
|
if (!matched && cleanText.length > 0 && cleanText.length <= 10 && sectionTextOnly.includes(cleanText)) {
|
|
190
194
|
score = 80
|
|
191
195
|
matched = true
|
|
192
|
-
|
|
196
|
+
// Find the actual line containing the text
|
|
197
|
+
const actualLine = findLineContainingText(lines, i, 5, cleanText)
|
|
198
|
+
matches.push({ line: actualLine, score, type: 'static' })
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
// Try matching first few words for longer text (static content)
|
|
@@ -198,7 +204,9 @@ async function searchAstroFile(
|
|
|
198
204
|
if (firstWords && sectionTextOnly.includes(firstWords)) {
|
|
199
205
|
score = 40
|
|
200
206
|
matched = true
|
|
201
|
-
|
|
207
|
+
// Find the actual line containing the text
|
|
208
|
+
const actualLine = findLineContainingText(lines, i, 5, firstWords)
|
|
209
|
+
matches.push({ line: actualLine, score, type: 'static' })
|
|
202
210
|
}
|
|
203
211
|
}
|
|
204
212
|
}
|
|
@@ -345,9 +353,10 @@ async function searchForPropInParents(dir: string, textContent: string): Promise
|
|
|
345
353
|
}
|
|
346
354
|
|
|
347
355
|
/**
|
|
348
|
-
* Extract complete tag snippet including content and indentation
|
|
356
|
+
* Extract complete tag snippet including content and indentation.
|
|
357
|
+
* Exported for use in html-processor to populate sourceSnippet.
|
|
349
358
|
*/
|
|
350
|
-
function extractCompleteTagSnippet(lines: string[], startLine: number, tag: string): string {
|
|
359
|
+
export function extractCompleteTagSnippet(lines: string[], startLine: number, tag: string): string {
|
|
351
360
|
const snippetLines: string[] = []
|
|
352
361
|
let depth = 0
|
|
353
362
|
let foundClosing = false
|
|
@@ -384,6 +393,68 @@ function extractCompleteTagSnippet(lines: string[], startLine: number, tag: stri
|
|
|
384
393
|
return snippetLines.join('\n')
|
|
385
394
|
}
|
|
386
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Extract innerHTML from a complete tag snippet.
|
|
398
|
+
* Given `<p class="foo">content here</p>`, returns `content here`.
|
|
399
|
+
*
|
|
400
|
+
* @param snippet - The complete tag snippet from source
|
|
401
|
+
* @param tag - The tag name (e.g., 'p', 'h1')
|
|
402
|
+
* @returns The innerHTML portion, or undefined if can't extract
|
|
403
|
+
*/
|
|
404
|
+
export function extractInnerHtmlFromSnippet(snippet: string, tag: string): string | undefined {
|
|
405
|
+
// Match opening tag (with any attributes) and extract content until closing tag
|
|
406
|
+
// Handle both single-line and multi-line cases
|
|
407
|
+
const openTagPattern = new RegExp(`<${tag}(?:\\s[^>]*)?>`, 'i')
|
|
408
|
+
const closeTagPattern = new RegExp(`</${tag}>`, 'i')
|
|
409
|
+
|
|
410
|
+
const openMatch = snippet.match(openTagPattern)
|
|
411
|
+
if (!openMatch) return undefined
|
|
412
|
+
|
|
413
|
+
const openTagEnd = openMatch.index! + openMatch[0].length
|
|
414
|
+
const closeMatch = snippet.match(closeTagPattern)
|
|
415
|
+
if (!closeMatch) return undefined
|
|
416
|
+
|
|
417
|
+
const closeTagStart = closeMatch.index!
|
|
418
|
+
|
|
419
|
+
// Extract content between opening and closing tags
|
|
420
|
+
if (closeTagStart > openTagEnd) {
|
|
421
|
+
return snippet.substring(openTagEnd, closeTagStart)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return undefined
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Read source file and extract the innerHTML at the specified line.
|
|
429
|
+
*
|
|
430
|
+
* @param sourceFile - Path to source file (relative to cwd)
|
|
431
|
+
* @param sourceLine - 1-indexed line number
|
|
432
|
+
* @param tag - The tag name
|
|
433
|
+
* @returns The innerHTML from source, or undefined if can't extract
|
|
434
|
+
*/
|
|
435
|
+
export async function extractSourceInnerHtml(
|
|
436
|
+
sourceFile: string,
|
|
437
|
+
sourceLine: number,
|
|
438
|
+
tag: string,
|
|
439
|
+
): Promise<string | undefined> {
|
|
440
|
+
try {
|
|
441
|
+
const filePath = path.isAbsolute(sourceFile)
|
|
442
|
+
? sourceFile
|
|
443
|
+
: path.join(process.cwd(), sourceFile)
|
|
444
|
+
|
|
445
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
446
|
+
const lines = content.split('\n')
|
|
447
|
+
|
|
448
|
+
// Extract the complete tag snippet
|
|
449
|
+
const snippet = extractCompleteTagSnippet(lines, sourceLine - 1, tag)
|
|
450
|
+
|
|
451
|
+
// Extract innerHTML from the snippet
|
|
452
|
+
return extractInnerHtmlFromSnippet(snippet, tag)
|
|
453
|
+
} catch {
|
|
454
|
+
return undefined
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
387
458
|
/**
|
|
388
459
|
* Extract variable references from frontmatter
|
|
389
460
|
*/
|
|
@@ -459,6 +530,22 @@ function collectSection(lines: string[], startLine: number, numLines: number): s
|
|
|
459
530
|
return text
|
|
460
531
|
}
|
|
461
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Find the actual line containing the matched text within a section
|
|
535
|
+
* Returns 1-indexed line number
|
|
536
|
+
*/
|
|
537
|
+
function findLineContainingText(lines: string[], startLine: number, numLines: number, searchText: string): number {
|
|
538
|
+
const normalizedSearch = searchText.toLowerCase()
|
|
539
|
+
for (let i = startLine; i < Math.min(startLine + numLines, lines.length); i++) {
|
|
540
|
+
const lineText = stripHtmlTags(lines[i] || '').toLowerCase()
|
|
541
|
+
if (lineText.includes(normalizedSearch)) {
|
|
542
|
+
return i + 1 // Return 1-indexed line number
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// If not found on a specific line, return the opening tag line
|
|
546
|
+
return startLine + 1
|
|
547
|
+
}
|
|
548
|
+
|
|
462
549
|
/**
|
|
463
550
|
* Strip HTML tags from text
|
|
464
551
|
*/
|
|
@@ -647,8 +734,10 @@ export async function findMarkdownSourceLocation(
|
|
|
647
734
|
let value = match[2]?.trim() || ''
|
|
648
735
|
|
|
649
736
|
// Handle quoted strings
|
|
650
|
-
if (
|
|
651
|
-
(value.startsWith("'
|
|
737
|
+
if (
|
|
738
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
739
|
+
|| (value.startsWith("'") && value.endsWith("'"))
|
|
740
|
+
) {
|
|
652
741
|
value = value.slice(1, -1)
|
|
653
742
|
}
|
|
654
743
|
|
|
@@ -720,8 +809,10 @@ export async function parseMarkdownContent(
|
|
|
720
809
|
let value = match[2]?.trim() || ''
|
|
721
810
|
|
|
722
811
|
// Handle quoted strings
|
|
723
|
-
if (
|
|
724
|
-
(value.startsWith("'
|
|
812
|
+
if (
|
|
813
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
814
|
+
|| (value.startsWith("'") && value.endsWith("'"))
|
|
815
|
+
) {
|
|
725
816
|
value = value.slice(1, -1)
|
|
726
817
|
}
|
|
727
818
|
|
|
@@ -769,3 +860,45 @@ function stripMarkdownSyntax(text: string): string {
|
|
|
769
860
|
.trim()
|
|
770
861
|
}
|
|
771
862
|
|
|
863
|
+
/**
|
|
864
|
+
* Enhance manifest entries with actual source snippets from source files.
|
|
865
|
+
* This reads the source files and extracts the innerHTML at the specified locations.
|
|
866
|
+
*
|
|
867
|
+
* @param entries - Manifest entries to enhance
|
|
868
|
+
* @returns Enhanced entries with sourceSnippet populated
|
|
869
|
+
*/
|
|
870
|
+
export async function enhanceManifestWithSourceSnippets(
|
|
871
|
+
entries: Record<string, ManifestEntry>,
|
|
872
|
+
): Promise<Record<string, ManifestEntry>> {
|
|
873
|
+
const enhanced: Record<string, ManifestEntry> = {}
|
|
874
|
+
|
|
875
|
+
// Process entries in parallel for better performance
|
|
876
|
+
const entryPromises = Object.entries(entries).map(async ([id, entry]) => {
|
|
877
|
+
// Skip if already has sourceSnippet or missing source info
|
|
878
|
+
if (entry.sourceSnippet || !entry.sourcePath || !entry.sourceLine || !entry.tag) {
|
|
879
|
+
return [id, entry] as const
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Extract the actual source innerHTML
|
|
883
|
+
const sourceSnippet = await extractSourceInnerHtml(
|
|
884
|
+
entry.sourcePath,
|
|
885
|
+
entry.sourceLine,
|
|
886
|
+
entry.tag,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
if (sourceSnippet) {
|
|
890
|
+
// Generate hash of source snippet for conflict detection
|
|
891
|
+
const sourceHash = generateSourceHash(sourceSnippet)
|
|
892
|
+
return [id, { ...entry, sourceSnippet, sourceHash }] as const
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return [id, entry] as const
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
const results = await Promise.all(entryPromises)
|
|
899
|
+
for (const [id, entry] of results) {
|
|
900
|
+
enhanced[id] = entry
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return enhanced
|
|
904
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { AvailableColors, ColorClasses, TailwindColor } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default Tailwind CSS v4 color names.
|
|
7
|
+
* These are available by default in Tailwind v4.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_TAILWIND_COLORS = [
|
|
10
|
+
'slate',
|
|
11
|
+
'gray',
|
|
12
|
+
'zinc',
|
|
13
|
+
'neutral',
|
|
14
|
+
'stone',
|
|
15
|
+
'red',
|
|
16
|
+
'orange',
|
|
17
|
+
'amber',
|
|
18
|
+
'yellow',
|
|
19
|
+
'lime',
|
|
20
|
+
'green',
|
|
21
|
+
'emerald',
|
|
22
|
+
'teal',
|
|
23
|
+
'cyan',
|
|
24
|
+
'sky',
|
|
25
|
+
'blue',
|
|
26
|
+
'indigo',
|
|
27
|
+
'violet',
|
|
28
|
+
'purple',
|
|
29
|
+
'fuchsia',
|
|
30
|
+
'pink',
|
|
31
|
+
'rose',
|
|
32
|
+
] as const
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Standard Tailwind color shades.
|
|
36
|
+
*/
|
|
37
|
+
export const STANDARD_SHADES = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950'] as const
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Special color values that don't have shades.
|
|
41
|
+
*/
|
|
42
|
+
export const SPECIAL_COLORS = ['transparent', 'current', 'inherit', 'white', 'black'] as const
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a regex pattern for matching color classes.
|
|
46
|
+
* Matches either:
|
|
47
|
+
* - Known default/special colors (e.g., bg-red, text-white)
|
|
48
|
+
* - Any color name followed by a shade number (e.g., bg-primary-500)
|
|
49
|
+
*/
|
|
50
|
+
function buildColorPattern(prefix: string): RegExp {
|
|
51
|
+
const colorNames = [...DEFAULT_TAILWIND_COLORS, ...SPECIAL_COLORS].join('|')
|
|
52
|
+
// Match either: known-color (with optional shade) OR any-name-with-shade (to support custom colors)
|
|
53
|
+
return new RegExp(`^${prefix}-((?:${colorNames})(?:-(\\d+))?|([a-z]+)-(\\d+))$`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Regex patterns to match Tailwind color classes.
|
|
58
|
+
* These patterns are specific to color utilities and won't match other utilities
|
|
59
|
+
* like text-lg, text-center, bg-fixed, etc.
|
|
60
|
+
*/
|
|
61
|
+
const COLOR_CLASS_PATTERNS = {
|
|
62
|
+
// Matches: bg-red-500, bg-primary-500, bg-white, bg-transparent
|
|
63
|
+
bg: buildColorPattern('bg'),
|
|
64
|
+
// Matches: text-red-500, text-primary-500, text-white (NOT text-lg, text-center)
|
|
65
|
+
text: buildColorPattern('text'),
|
|
66
|
+
// Matches: border-red-500, border-primary-500
|
|
67
|
+
border: buildColorPattern('border'),
|
|
68
|
+
// Matches: hover:bg-red-500
|
|
69
|
+
hoverBg: buildColorPattern('hover:bg'),
|
|
70
|
+
// Matches: hover:text-red-500
|
|
71
|
+
hoverText: buildColorPattern('hover:text'),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse Tailwind v4 CSS config to extract available colors.
|
|
76
|
+
* Tailwind v4 uses CSS-based configuration with @theme directive.
|
|
77
|
+
*
|
|
78
|
+
* Example CSS:
|
|
79
|
+
* ```css
|
|
80
|
+
* @theme {
|
|
81
|
+
* --color-primary-50: #eff6ff;
|
|
82
|
+
* --color-primary-500: #3b82f6;
|
|
83
|
+
* --color-accent: #f59e0b;
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export async function parseTailwindConfig(projectRoot: string = process.cwd()): Promise<AvailableColors> {
|
|
88
|
+
// Tailwind v4 CSS files to search
|
|
89
|
+
const cssFiles = [
|
|
90
|
+
'src/styles/global.css',
|
|
91
|
+
'src/styles/tailwind.css',
|
|
92
|
+
'src/styles/app.css',
|
|
93
|
+
'src/app.css',
|
|
94
|
+
'src/global.css',
|
|
95
|
+
'src/index.css',
|
|
96
|
+
'app/globals.css',
|
|
97
|
+
'styles/globals.css',
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
let customColors: TailwindColor[] = []
|
|
101
|
+
|
|
102
|
+
for (const cssFile of cssFiles) {
|
|
103
|
+
const fullPath = path.join(projectRoot, cssFile)
|
|
104
|
+
try {
|
|
105
|
+
const content = await fs.readFile(fullPath, 'utf-8')
|
|
106
|
+
customColors = extractColorsFromCss(content)
|
|
107
|
+
if (customColors.length > 0) {
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// File doesn't exist, continue
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Build default colors list
|
|
116
|
+
const defaultColors: TailwindColor[] = DEFAULT_TAILWIND_COLORS.map(name => ({
|
|
117
|
+
name,
|
|
118
|
+
shades: [...STANDARD_SHADES],
|
|
119
|
+
isCustom: false,
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
// Add special colors (no shades)
|
|
123
|
+
const specialColors: TailwindColor[] = SPECIAL_COLORS.map(name => ({
|
|
124
|
+
name,
|
|
125
|
+
shades: [],
|
|
126
|
+
isCustom: false,
|
|
127
|
+
}))
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
colors: [...specialColors, ...defaultColors, ...customColors],
|
|
131
|
+
defaultColors: [...SPECIAL_COLORS, ...DEFAULT_TAILWIND_COLORS],
|
|
132
|
+
customColors: customColors.map(c => c.name),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Extract custom colors from Tailwind v4 CSS @theme block.
|
|
138
|
+
*
|
|
139
|
+
* Looks for patterns like:
|
|
140
|
+
* - --color-primary-50: #value;
|
|
141
|
+
* - --color-primary: #value;
|
|
142
|
+
* - --color-accent-500: oklch(...);
|
|
143
|
+
*/
|
|
144
|
+
function extractColorsFromCss(content: string): TailwindColor[] {
|
|
145
|
+
const colors = new Map<string, Set<string>>()
|
|
146
|
+
|
|
147
|
+
// Find @theme blocks
|
|
148
|
+
const themeBlockPattern = /@theme\s*\{([^}]+)\}/gs
|
|
149
|
+
let themeMatch: RegExpExecArray | null
|
|
150
|
+
|
|
151
|
+
while ((themeMatch = themeBlockPattern.exec(content)) !== null) {
|
|
152
|
+
const themeContent = themeMatch[1]
|
|
153
|
+
if (!themeContent) continue
|
|
154
|
+
|
|
155
|
+
// Find all --color-* definitions
|
|
156
|
+
// Pattern: --color-{name}-{shade}: value; or --color-{name}: value;
|
|
157
|
+
const colorVarPattern = /--color-([a-z]+)(?:-(\d+))?:/gi
|
|
158
|
+
let colorMatch: RegExpExecArray | null
|
|
159
|
+
|
|
160
|
+
while ((colorMatch = colorVarPattern.exec(themeContent)) !== null) {
|
|
161
|
+
const colorName = colorMatch[1]?.toLowerCase()
|
|
162
|
+
const shade = colorMatch[2]
|
|
163
|
+
|
|
164
|
+
if (!colorName) continue
|
|
165
|
+
|
|
166
|
+
// Skip if it's a default color
|
|
167
|
+
if (DEFAULT_TAILWIND_COLORS.includes(colorName as any)) {
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!colors.has(colorName)) {
|
|
172
|
+
colors.set(colorName, new Set())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (shade) {
|
|
176
|
+
colors.get(colorName)!.add(shade)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Also check for inline @theme definitions (Tailwind v4 can be inline too)
|
|
182
|
+
// Pattern: @theme inline { ... }
|
|
183
|
+
const inlineThemePattern = /@theme\s+inline\s*\{([^}]+)\}/gs
|
|
184
|
+
let inlineMatch: RegExpExecArray | null
|
|
185
|
+
|
|
186
|
+
while ((inlineMatch = inlineThemePattern.exec(content)) !== null) {
|
|
187
|
+
const themeContent = inlineMatch[1]
|
|
188
|
+
if (!themeContent) continue
|
|
189
|
+
|
|
190
|
+
const colorVarPattern = /--color-([a-z]+)(?:-(\d+))?:/gi
|
|
191
|
+
let colorMatch: RegExpExecArray | null
|
|
192
|
+
|
|
193
|
+
while ((colorMatch = colorVarPattern.exec(themeContent)) !== null) {
|
|
194
|
+
const colorName = colorMatch[1]?.toLowerCase()
|
|
195
|
+
const shade = colorMatch[2]
|
|
196
|
+
|
|
197
|
+
if (!colorName) continue
|
|
198
|
+
|
|
199
|
+
if (DEFAULT_TAILWIND_COLORS.includes(colorName as any)) {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!colors.has(colorName)) {
|
|
204
|
+
colors.set(colorName, new Set())
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (shade) {
|
|
208
|
+
colors.get(colorName)!.add(shade)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Convert to TailwindColor array
|
|
214
|
+
const result: TailwindColor[] = []
|
|
215
|
+
for (const [name, shades] of colors) {
|
|
216
|
+
const sortedShades = Array.from(shades).sort((a, b) => parseInt(a) - parseInt(b))
|
|
217
|
+
result.push({
|
|
218
|
+
name,
|
|
219
|
+
shades: sortedShades.length > 0 ? sortedShades : [],
|
|
220
|
+
isCustom: true,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return result
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Extract color classes from an element's class attribute.
|
|
229
|
+
*/
|
|
230
|
+
export function extractColorClasses(classAttr: string | null | undefined): ColorClasses | undefined {
|
|
231
|
+
if (!classAttr) return undefined
|
|
232
|
+
|
|
233
|
+
const classes = classAttr.split(/\s+/).filter(Boolean)
|
|
234
|
+
const colorClasses: ColorClasses = {}
|
|
235
|
+
const allColorClasses: string[] = []
|
|
236
|
+
|
|
237
|
+
for (const cls of classes) {
|
|
238
|
+
// Check each pattern
|
|
239
|
+
for (const [key, pattern] of Object.entries(COLOR_CLASS_PATTERNS)) {
|
|
240
|
+
const match = cls.match(pattern)
|
|
241
|
+
if (match) {
|
|
242
|
+
allColorClasses.push(cls)
|
|
243
|
+
// Assign to appropriate field
|
|
244
|
+
if (!(key in colorClasses)) {
|
|
245
|
+
;(colorClasses as any)[key] = cls
|
|
246
|
+
}
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (allColorClasses.length === 0) {
|
|
253
|
+
return undefined
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
colorClasses.allColorClasses = allColorClasses
|
|
257
|
+
return colorClasses
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check if a class is a color class.
|
|
262
|
+
*/
|
|
263
|
+
export function isColorClass(className: string): boolean {
|
|
264
|
+
return Object.values(COLOR_CLASS_PATTERNS).some(pattern => pattern.test(className))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Generate a new class string with a color class replaced.
|
|
269
|
+
* @param currentClasses - Current class attribute value
|
|
270
|
+
* @param oldColorClass - The color class to replace (e.g., 'bg-blue-500')
|
|
271
|
+
* @param newColorClass - The new color class (e.g., 'bg-red-500')
|
|
272
|
+
* @returns New class string with the replacement
|
|
273
|
+
*/
|
|
274
|
+
export function replaceColorClass(
|
|
275
|
+
currentClasses: string,
|
|
276
|
+
oldColorClass: string,
|
|
277
|
+
newColorClass: string,
|
|
278
|
+
): string {
|
|
279
|
+
const classes = currentClasses.split(/\s+/).filter(Boolean)
|
|
280
|
+
const newClasses = classes.map(cls => cls === oldColorClass ? newColorClass : cls)
|
|
281
|
+
return newClasses.join(' ')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get the color type from a color class.
|
|
286
|
+
* @param colorClass - e.g., 'bg-blue-500', 'text-white', 'hover:bg-red-600'
|
|
287
|
+
* @returns The type: 'bg', 'text', 'border', 'hoverBg', 'hoverText', or undefined
|
|
288
|
+
*/
|
|
289
|
+
export function getColorType(colorClass: string): keyof ColorClasses | undefined {
|
|
290
|
+
for (const [key, pattern] of Object.entries(COLOR_CLASS_PATTERNS)) {
|
|
291
|
+
if (pattern.test(colorClass)) {
|
|
292
|
+
return key as keyof ColorClasses
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return undefined
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Parse a color class into its components.
|
|
300
|
+
* @param colorClass - e.g., 'bg-blue-500', 'text-white', 'hover:bg-red-600'
|
|
301
|
+
* @returns Object with prefix, colorName, and shade (if any)
|
|
302
|
+
*/
|
|
303
|
+
export function parseColorClass(colorClass: string): {
|
|
304
|
+
prefix: string
|
|
305
|
+
colorName: string
|
|
306
|
+
shade?: string
|
|
307
|
+
isHover: boolean
|
|
308
|
+
} | undefined {
|
|
309
|
+
// Handle hover prefix
|
|
310
|
+
const isHover = colorClass.startsWith('hover:')
|
|
311
|
+
const classWithoutHover = isHover ? colorClass.slice(6) : colorClass
|
|
312
|
+
|
|
313
|
+
// Match prefix-color-shade or prefix-color
|
|
314
|
+
const match = classWithoutHover.match(/^(bg|text|border)-([a-z]+)(?:-(\d+))?$/)
|
|
315
|
+
|
|
316
|
+
if (!match) return undefined
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
prefix: isHover ? `hover:${match[1]}` : match[1]!,
|
|
320
|
+
colorName: match[2]!,
|
|
321
|
+
shade: match[3],
|
|
322
|
+
isHover,
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Build a color class from components.
|
|
328
|
+
*/
|
|
329
|
+
export function buildColorClass(
|
|
330
|
+
prefix: string,
|
|
331
|
+
colorName: string,
|
|
332
|
+
shade?: string,
|
|
333
|
+
): string {
|
|
334
|
+
if (shade) {
|
|
335
|
+
return `${prefix}-${colorName}-${shade}`
|
|
336
|
+
}
|
|
337
|
+
return `${prefix}-${colorName}`
|
|
338
|
+
}
|