@nuasite/cms-marker 0.0.53 → 0.0.55
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 +5 -5
- 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 +117 -6
- 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
|
|
@@ -351,9 +353,10 @@ async function searchForPropInParents(dir: string, textContent: string): Promise
|
|
|
351
353
|
}
|
|
352
354
|
|
|
353
355
|
/**
|
|
354
|
-
* 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.
|
|
355
358
|
*/
|
|
356
|
-
function extractCompleteTagSnippet(lines: string[], startLine: number, tag: string): string {
|
|
359
|
+
export function extractCompleteTagSnippet(lines: string[], startLine: number, tag: string): string {
|
|
357
360
|
const snippetLines: string[] = []
|
|
358
361
|
let depth = 0
|
|
359
362
|
let foundClosing = false
|
|
@@ -390,6 +393,68 @@ function extractCompleteTagSnippet(lines: string[], startLine: number, tag: stri
|
|
|
390
393
|
return snippetLines.join('\n')
|
|
391
394
|
}
|
|
392
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
|
+
|
|
393
458
|
/**
|
|
394
459
|
* Extract variable references from frontmatter
|
|
395
460
|
*/
|
|
@@ -669,8 +734,10 @@ export async function findMarkdownSourceLocation(
|
|
|
669
734
|
let value = match[2]?.trim() || ''
|
|
670
735
|
|
|
671
736
|
// Handle quoted strings
|
|
672
|
-
if (
|
|
673
|
-
(value.startsWith("'
|
|
737
|
+
if (
|
|
738
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
739
|
+
|| (value.startsWith("'") && value.endsWith("'"))
|
|
740
|
+
) {
|
|
674
741
|
value = value.slice(1, -1)
|
|
675
742
|
}
|
|
676
743
|
|
|
@@ -742,8 +809,10 @@ export async function parseMarkdownContent(
|
|
|
742
809
|
let value = match[2]?.trim() || ''
|
|
743
810
|
|
|
744
811
|
// Handle quoted strings
|
|
745
|
-
if (
|
|
746
|
-
(value.startsWith("'
|
|
812
|
+
if (
|
|
813
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
814
|
+
|| (value.startsWith("'") && value.endsWith("'"))
|
|
815
|
+
) {
|
|
747
816
|
value = value.slice(1, -1)
|
|
748
817
|
}
|
|
749
818
|
|
|
@@ -791,3 +860,45 @@ function stripMarkdownSyntax(text: string): string {
|
|
|
791
860
|
.trim()
|
|
792
861
|
}
|
|
793
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
|
+
}
|
package/src/tsconfig.json
CHANGED
package/src/types.ts
CHANGED
|
@@ -27,14 +27,95 @@ export interface ComponentDefinition {
|
|
|
27
27
|
slots?: string[]
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Context around an element for resilient matching when exact match fails */
|
|
31
|
+
export interface SourceContext {
|
|
32
|
+
/** Text content of the preceding sibling element */
|
|
33
|
+
precedingText?: string
|
|
34
|
+
/** Text content of the following sibling element */
|
|
35
|
+
followingText?: string
|
|
36
|
+
/** Parent element's tag name */
|
|
37
|
+
parentTag?: string
|
|
38
|
+
/** Position among siblings (0-indexed) */
|
|
39
|
+
siblingIndex?: number
|
|
40
|
+
/** Parent element's class attribute */
|
|
41
|
+
parentClasses?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Image metadata for better tracking and integrity */
|
|
45
|
+
export interface ImageMetadata {
|
|
46
|
+
/** Image source URL */
|
|
47
|
+
src: string
|
|
48
|
+
/** Alt text */
|
|
49
|
+
alt: string
|
|
50
|
+
/** SHA256 hash of image content (for integrity checking) */
|
|
51
|
+
hash?: string
|
|
52
|
+
/** Image dimensions */
|
|
53
|
+
dimensions?: { width: number; height: number }
|
|
54
|
+
/** Responsive image srcset */
|
|
55
|
+
srcSet?: string
|
|
56
|
+
/** Image sizes attribute */
|
|
57
|
+
sizes?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Content constraints for validation */
|
|
61
|
+
export interface ContentConstraints {
|
|
62
|
+
/** Maximum content length */
|
|
63
|
+
maxLength?: number
|
|
64
|
+
/** Minimum content length */
|
|
65
|
+
minLength?: number
|
|
66
|
+
/** Regex pattern for validation */
|
|
67
|
+
pattern?: string
|
|
68
|
+
/** Allowed HTML tags for rich text content */
|
|
69
|
+
allowedTags?: string[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Represents a single Tailwind color with its shades */
|
|
73
|
+
export interface TailwindColor {
|
|
74
|
+
/** Color name (e.g., 'red', 'blue', 'primary') */
|
|
75
|
+
name: string
|
|
76
|
+
/** Available shades (e.g., ['50', '100', '200', ..., '900', '950']) */
|
|
77
|
+
shades: string[]
|
|
78
|
+
/** Whether this is a custom/theme color vs default Tailwind */
|
|
79
|
+
isCustom?: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Color classes currently applied to an element */
|
|
83
|
+
export interface ColorClasses {
|
|
84
|
+
/** Background color class (e.g., 'bg-blue-500') */
|
|
85
|
+
bg?: string
|
|
86
|
+
/** Text color class (e.g., 'text-white') */
|
|
87
|
+
text?: string
|
|
88
|
+
/** Border color class (e.g., 'border-blue-600') */
|
|
89
|
+
border?: string
|
|
90
|
+
/** Hover background color class (e.g., 'hover:bg-blue-600') */
|
|
91
|
+
hoverBg?: string
|
|
92
|
+
/** Hover text color class (e.g., 'hover:text-gray-100') */
|
|
93
|
+
hoverText?: string
|
|
94
|
+
/** All color-related classes as found in the element */
|
|
95
|
+
allColorClasses?: string[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Available colors palette from Tailwind config */
|
|
99
|
+
export interface AvailableColors {
|
|
100
|
+
/** All available colors with their shades */
|
|
101
|
+
colors: TailwindColor[]
|
|
102
|
+
/** Default Tailwind color names */
|
|
103
|
+
defaultColors: string[]
|
|
104
|
+
/** Custom/theme color names */
|
|
105
|
+
customColors: string[]
|
|
106
|
+
}
|
|
107
|
+
|
|
30
108
|
export interface ManifestEntry {
|
|
31
109
|
id: string
|
|
32
110
|
tag: string
|
|
111
|
+
/** Plain text content (for display/search) */
|
|
33
112
|
text: string
|
|
113
|
+
/** HTML content when element contains inline styling (strong, em, etc.) */
|
|
114
|
+
html?: string
|
|
34
115
|
sourcePath?: string
|
|
35
116
|
sourceLine?: number
|
|
36
117
|
sourceSnippet?: string
|
|
37
|
-
sourceType?: 'static' | 'variable' | 'prop' | 'computed' | 'collection'
|
|
118
|
+
sourceType?: 'static' | 'variable' | 'prop' | 'computed' | 'collection' | 'image'
|
|
38
119
|
variableName?: string
|
|
39
120
|
childCmsIds?: string[]
|
|
40
121
|
parentComponentId?: string
|
|
@@ -42,6 +123,27 @@ export interface ManifestEntry {
|
|
|
42
123
|
collectionName?: string
|
|
43
124
|
/** Entry slug for collection entries (e.g., '3d-tisk') */
|
|
44
125
|
collectionSlug?: string
|
|
126
|
+
/** Path to the markdown content file (e.g., 'src/content/blog/my-post.md') */
|
|
127
|
+
contentPath?: string
|
|
128
|
+
/** Image source URL (for image entries) - deprecated, use imageMetadata */
|
|
129
|
+
imageSrc?: string
|
|
130
|
+
/** Image alt text (for image entries) - deprecated, use imageMetadata */
|
|
131
|
+
imageAlt?: string
|
|
132
|
+
|
|
133
|
+
// === Robustness fields ===
|
|
134
|
+
|
|
135
|
+
/** Stable ID derived from content + context hash, survives rebuilds */
|
|
136
|
+
stableId?: string
|
|
137
|
+
/** SHA256 hash of sourceSnippet at generation time for conflict detection */
|
|
138
|
+
sourceHash?: string
|
|
139
|
+
/** Context around the element for resilient matching */
|
|
140
|
+
sourceContext?: SourceContext
|
|
141
|
+
/** Image metadata for img elements (replaces imageSrc/imageAlt) */
|
|
142
|
+
imageMetadata?: ImageMetadata
|
|
143
|
+
/** Content validation constraints */
|
|
144
|
+
constraints?: ContentConstraints
|
|
145
|
+
/** Color classes applied to this element (for buttons, etc.) */
|
|
146
|
+
colorClasses?: ColorClasses
|
|
45
147
|
}
|
|
46
148
|
|
|
47
149
|
export interface ComponentInstance {
|
|
@@ -73,10 +175,30 @@ export interface CollectionEntry {
|
|
|
73
175
|
wrapperId?: string
|
|
74
176
|
}
|
|
75
177
|
|
|
178
|
+
/** Manifest metadata for versioning and conflict detection */
|
|
179
|
+
export interface ManifestMetadata {
|
|
180
|
+
/** Manifest schema version */
|
|
181
|
+
version: string
|
|
182
|
+
/** ISO timestamp when manifest was generated */
|
|
183
|
+
generatedAt: string
|
|
184
|
+
/** Build system that generated the manifest (e.g., 'astro', 'vite') */
|
|
185
|
+
generatedBy?: string
|
|
186
|
+
/** Build ID for correlation */
|
|
187
|
+
buildId?: string
|
|
188
|
+
/** SHA256 hash of all entry content for quick drift detection */
|
|
189
|
+
contentHash?: string
|
|
190
|
+
/** Per-source-file hashes for granular conflict detection */
|
|
191
|
+
sourceFileHashes?: Record<string, string>
|
|
192
|
+
}
|
|
193
|
+
|
|
76
194
|
export interface CmsManifest {
|
|
195
|
+
/** Manifest metadata for versioning and conflict detection */
|
|
196
|
+
metadata?: ManifestMetadata
|
|
77
197
|
entries: Record<string, ManifestEntry>
|
|
78
198
|
components: Record<string, ComponentInstance>
|
|
79
199
|
componentDefinitions: Record<string, ComponentDefinition>
|
|
80
200
|
/** Content collection entries indexed by "collectionName/slug" */
|
|
81
201
|
collections?: Record<string, CollectionEntry>
|
|
202
|
+
/** Available Tailwind colors from the project's config */
|
|
203
|
+
availableColors?: AvailableColors
|
|
82
204
|
}
|