@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.
Files changed (39) hide show
  1. package/README.md +0 -4
  2. package/dist/types/astro-transform.d.ts +21 -0
  3. package/dist/types/astro-transform.d.ts.map +1 -0
  4. package/dist/types/build-processor.d.ts +10 -0
  5. package/dist/types/build-processor.d.ts.map +1 -0
  6. package/dist/types/component-registry.d.ts +63 -0
  7. package/dist/types/component-registry.d.ts.map +1 -0
  8. package/dist/types/dev-middleware.d.ts +7 -0
  9. package/dist/types/dev-middleware.d.ts.map +1 -0
  10. package/dist/types/html-processor.d.ts +51 -0
  11. package/dist/types/html-processor.d.ts.map +1 -0
  12. package/dist/types/index.d.ts +7 -0
  13. package/dist/types/index.d.ts.map +1 -0
  14. package/dist/types/manifest-writer.d.ts +75 -0
  15. package/dist/types/manifest-writer.d.ts.map +1 -0
  16. package/dist/types/source-finder.d.ts +97 -0
  17. package/dist/types/source-finder.d.ts.map +1 -0
  18. package/dist/types/tailwind-colors.d.ts +66 -0
  19. package/dist/types/tailwind-colors.d.ts.map +1 -0
  20. package/dist/types/tsconfig.tsbuildinfo +1 -0
  21. package/dist/types/types.d.ts +195 -0
  22. package/dist/types/types.d.ts.map +1 -0
  23. package/dist/types/utils.d.ts +38 -0
  24. package/dist/types/utils.d.ts.map +1 -0
  25. package/dist/types/vite-plugin.d.ts +14 -0
  26. package/dist/types/vite-plugin.d.ts.map +1 -0
  27. package/package.json +1 -1
  28. package/src/astro-transform.ts +4 -4
  29. package/src/build-processor.ts +1 -1
  30. package/src/component-registry.ts +2 -2
  31. package/src/dev-middleware.ts +6 -6
  32. package/src/html-processor.ts +180 -6
  33. package/src/index.ts +0 -1
  34. package/src/manifest-writer.ts +47 -1
  35. package/src/source-finder.ts +142 -9
  36. package/src/tailwind-colors.ts +338 -0
  37. package/src/tsconfig.json +1 -1
  38. package/src/types.ts +123 -1
  39. package/src/utils.ts +99 -0
@@ -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
- matches.push({ line: i + 1, score, type: 'static' })
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
- matches.push({ line: i + 1, score, type: 'static' })
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
- matches.push({ line: i + 1, score, type: 'static' })
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 ((value.startsWith('"') && value.endsWith('"')) ||
651
- (value.startsWith("'") && value.endsWith("'"))) {
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 ((value.startsWith('"') && value.endsWith('"')) ||
724
- (value.startsWith("'") && value.endsWith("'"))) {
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
+ }
package/src/tsconfig.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "extends": "../tsconfig.settings.json",
3
3
  "compilerOptions": {
4
- "outDir": "../dist/types"
4
+ "outDir": "../dist/types",
5
5
  }
6
6
  }