@nuasite/cms 0.1.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 +237 -0
- package/dist/src/build-processor.d.ts +20 -0
- package/dist/src/build-processor.d.ts.map +1 -0
- package/dist/src/collection-scanner.d.ts +6 -0
- package/dist/src/collection-scanner.d.ts.map +1 -0
- package/dist/src/component-registry.d.ts +63 -0
- package/dist/src/component-registry.d.ts.map +1 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/dev-middleware.d.ts +20 -0
- package/dist/src/dev-middleware.d.ts.map +1 -0
- package/dist/src/editor/ai.d.ts +60 -0
- package/dist/src/editor/ai.d.ts.map +1 -0
- package/dist/src/editor/api.d.ts +140 -0
- package/dist/src/editor/api.d.ts.map +1 -0
- package/dist/src/editor/color-utils.d.ts +106 -0
- package/dist/src/editor/color-utils.d.ts.map +1 -0
- package/dist/src/editor/components/ai-chat.d.ts +11 -0
- package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
- package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
- package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
- package/dist/src/editor/components/attribute-editor.d.ts +5 -0
- package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
- package/dist/src/editor/components/block-editor.d.ts +12 -0
- package/dist/src/editor/components/block-editor.d.ts.map +1 -0
- package/dist/src/editor/components/collections-browser.d.ts +2 -0
- package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
- package/dist/src/editor/components/color-toolbar.d.ts +12 -0
- package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
- package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
- package/dist/src/editor/components/create-page-modal.d.ts +2 -0
- package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
- package/dist/src/editor/components/editable-highlights.d.ts +9 -0
- package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
- package/dist/src/editor/components/error-boundary.d.ts +32 -0
- package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
- package/dist/src/editor/components/fields.d.ts +75 -0
- package/dist/src/editor/components/fields.d.ts.map +1 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
- package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
- package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/image-overlay.d.ts +12 -0
- package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
- package/dist/src/editor/components/media-library.d.ts +2 -0
- package/dist/src/editor/components/media-library.d.ts.map +1 -0
- package/dist/src/editor/components/outline.d.ts +21 -0
- package/dist/src/editor/components/outline.d.ts.map +1 -0
- package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
- package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
- package/dist/src/editor/components/seo-editor.d.ts +2 -0
- package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
- package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast.d.ts +7 -0
- package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
- package/dist/src/editor/components/toast/types.d.ts +7 -0
- package/dist/src/editor/components/toast/types.d.ts.map +1 -0
- package/dist/src/editor/components/toolbar.d.ts +21 -0
- package/dist/src/editor/components/toolbar.d.ts.map +1 -0
- package/dist/src/editor/config.d.ts +4 -0
- package/dist/src/editor/config.d.ts.map +1 -0
- package/dist/src/editor/constants.d.ts +101 -0
- package/dist/src/editor/constants.d.ts.map +1 -0
- package/dist/src/editor/context.d.ts +14 -0
- package/dist/src/editor/context.d.ts.map +1 -0
- package/dist/src/editor/dom.d.ts +77 -0
- package/dist/src/editor/dom.d.ts.map +1 -0
- package/dist/src/editor/editor.d.ts +64 -0
- package/dist/src/editor/editor.d.ts.map +1 -0
- package/dist/src/editor/history.d.ts +20 -0
- package/dist/src/editor/history.d.ts.map +1 -0
- package/dist/src/editor/hooks/index.d.ts +14 -0
- package/dist/src/editor/hooks/index.d.ts.map +1 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
- package/dist/src/editor/hooks/utils.d.ts +32 -0
- package/dist/src/editor/hooks/utils.d.ts.map +1 -0
- package/dist/src/editor/index.d.ts +12 -0
- package/dist/src/editor/index.d.ts.map +1 -0
- package/dist/src/editor/lib/cn.d.ts +3 -0
- package/dist/src/editor/lib/cn.d.ts.map +1 -0
- package/dist/src/editor/manifest.d.ts +19 -0
- package/dist/src/editor/manifest.d.ts.map +1 -0
- package/dist/src/editor/markdown-api.d.ts +36 -0
- package/dist/src/editor/markdown-api.d.ts.map +1 -0
- package/dist/src/editor/signals.d.ts +242 -0
- package/dist/src/editor/signals.d.ts.map +1 -0
- package/dist/src/editor/storage.d.ts +27 -0
- package/dist/src/editor/storage.d.ts.map +1 -0
- package/dist/src/editor/text-styling.d.ts +350 -0
- package/dist/src/editor/text-styling.d.ts.map +1 -0
- package/dist/src/editor/themes.d.ts +38 -0
- package/dist/src/editor/themes.d.ts.map +1 -0
- package/dist/src/editor/types.d.ts +454 -0
- package/dist/src/editor/types.d.ts.map +1 -0
- package/dist/src/error-collector.d.ts +56 -0
- package/dist/src/error-collector.d.ts.map +1 -0
- package/dist/src/handlers/component-ops.d.ts +34 -0
- package/dist/src/handlers/component-ops.d.ts.map +1 -0
- package/dist/src/handlers/markdown-ops.d.ts +41 -0
- package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
- package/dist/src/handlers/request-utils.d.ts +20 -0
- package/dist/src/handlers/request-utils.d.ts.map +1 -0
- package/dist/src/handlers/source-writer.d.ts +51 -0
- package/dist/src/handlers/source-writer.d.ts.map +1 -0
- package/dist/src/html-processor.d.ts +63 -0
- package/dist/src/html-processor.d.ts.map +1 -0
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/manifest-writer.d.ts +111 -0
- package/dist/src/manifest-writer.d.ts.map +1 -0
- package/dist/src/media/contember.d.ts +15 -0
- package/dist/src/media/contember.d.ts.map +1 -0
- package/dist/src/media/local.d.ts +9 -0
- package/dist/src/media/local.d.ts.map +1 -0
- package/dist/src/media/s3.d.ts +12 -0
- package/dist/src/media/s3.d.ts.map +1 -0
- package/dist/src/media/types.d.ts +40 -0
- package/dist/src/media/types.d.ts.map +1 -0
- package/dist/src/preview-generator.d.ts +19 -0
- package/dist/src/preview-generator.d.ts.map +1 -0
- package/dist/src/seo-processor.d.ts +23 -0
- package/dist/src/seo-processor.d.ts.map +1 -0
- package/dist/src/source-finder/ast-extractors.d.ts +35 -0
- package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
- package/dist/src/source-finder/ast-parser.d.ts +16 -0
- package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
- package/dist/src/source-finder/cache.d.ts +18 -0
- package/dist/src/source-finder/cache.d.ts.map +1 -0
- package/dist/src/source-finder/collection-finder.d.ts +29 -0
- package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
- package/dist/src/source-finder/element-finder.d.ts +42 -0
- package/dist/src/source-finder/element-finder.d.ts.map +1 -0
- package/dist/src/source-finder/image-finder.d.ts +24 -0
- package/dist/src/source-finder/image-finder.d.ts.map +1 -0
- package/dist/src/source-finder/index.d.ts +9 -0
- package/dist/src/source-finder/index.d.ts.map +1 -0
- package/dist/src/source-finder/search-index.d.ts +27 -0
- package/dist/src/source-finder/search-index.d.ts.map +1 -0
- package/dist/src/source-finder/snippet-utils.d.ts +90 -0
- package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
- package/dist/src/source-finder/source-lookup.d.ts +16 -0
- package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
- package/dist/src/source-finder/types.d.ts +167 -0
- package/dist/src/source-finder/types.d.ts.map +1 -0
- package/dist/src/source-finder/variable-extraction.d.ts +37 -0
- package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
- package/dist/src/tailwind-colors.d.ts +54 -0
- package/dist/src/tailwind-colors.d.ts.map +1 -0
- package/dist/src/tsconfig.tsbuildinfo +1 -0
- package/dist/src/types.d.ts +367 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +61 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/vite-plugin.d.ts +14 -0
- package/dist/src/vite-plugin.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/build-processor.ts +784 -0
- package/src/collection-scanner.ts +304 -0
- package/src/component-registry.ts +393 -0
- package/src/config.ts +74 -0
- package/src/dev-middleware.ts +525 -0
- package/src/dist/src/tsconfig.tsbuildinfo +1 -0
- package/src/editor/ai.ts +185 -0
- package/src/editor/api.ts +513 -0
- package/src/editor/color-utils.ts +556 -0
- package/src/editor/components/ai-chat.tsx +632 -0
- package/src/editor/components/ai-tooltip.tsx +179 -0
- package/src/editor/components/attribute-editor.tsx +596 -0
- package/src/editor/components/block-editor.tsx +546 -0
- package/src/editor/components/collections-browser.tsx +248 -0
- package/src/editor/components/color-toolbar.tsx +314 -0
- package/src/editor/components/confirm-dialog.tsx +69 -0
- package/src/editor/components/create-page-modal.tsx +163 -0
- package/src/editor/components/editable-highlights.tsx +260 -0
- package/src/editor/components/error-boundary.tsx +87 -0
- package/src/editor/components/fields.tsx +387 -0
- package/src/editor/components/frontmatter-fields.tsx +469 -0
- package/src/editor/components/highlight-overlay.ts +229 -0
- package/src/editor/components/image-overlay.tsx +230 -0
- package/src/editor/components/markdown-editor-overlay.tsx +505 -0
- package/src/editor/components/markdown-inline-editor.tsx +780 -0
- package/src/editor/components/media-library.tsx +297 -0
- package/src/editor/components/outline.tsx +402 -0
- package/src/editor/components/redirect-countdown.tsx +45 -0
- package/src/editor/components/seo-editor.tsx +498 -0
- package/src/editor/components/text-style-toolbar.tsx +362 -0
- package/src/editor/components/toast/toast-container.tsx +15 -0
- package/src/editor/components/toast/toast.tsx +49 -0
- package/src/editor/components/toast/types.ts +7 -0
- package/src/editor/components/toolbar.tsx +366 -0
- package/src/editor/config.ts +12 -0
- package/src/editor/constants.ts +106 -0
- package/src/editor/context.tsx +38 -0
- package/src/editor/dom.ts +357 -0
- package/src/editor/editor.ts +1510 -0
- package/src/editor/env.d.ts +4 -0
- package/src/editor/history.ts +355 -0
- package/src/editor/hooks/index.ts +19 -0
- package/src/editor/hooks/useAIHandlers.ts +345 -0
- package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
- package/src/editor/hooks/useElementDetection.ts +284 -0
- package/src/editor/hooks/useImageHoverDetection.ts +102 -0
- package/src/editor/hooks/useTextSelection.ts +187 -0
- package/src/editor/hooks/useTooltipState.ts +126 -0
- package/src/editor/hooks/utils.ts +101 -0
- package/src/editor/index.tsx +481 -0
- package/src/editor/lib/cn.ts +4 -0
- package/src/editor/manifest.ts +25 -0
- package/src/editor/markdown-api.ts +209 -0
- package/src/editor/signals.ts +1351 -0
- package/src/editor/storage.ts +266 -0
- package/src/editor/styles.css +465 -0
- package/src/editor/text-styling.ts +773 -0
- package/src/editor/themes.ts +210 -0
- package/src/editor/types.ts +591 -0
- package/src/error-collector.ts +106 -0
- package/src/handlers/component-ops.ts +463 -0
- package/src/handlers/markdown-ops.ts +202 -0
- package/src/handlers/request-utils.ts +151 -0
- package/src/handlers/source-writer.ts +649 -0
- package/src/html-processor.ts +1108 -0
- package/src/index.ts +284 -0
- package/src/manifest-writer.ts +371 -0
- package/src/media/contember.ts +84 -0
- package/src/media/local.ts +114 -0
- package/src/media/s3.ts +133 -0
- package/src/media/types.ts +33 -0
- package/src/preview-generator.ts +293 -0
- package/src/seo-processor.ts +567 -0
- package/src/source-finder/ast-extractors.ts +185 -0
- package/src/source-finder/ast-parser.ts +150 -0
- package/src/source-finder/cache.ts +76 -0
- package/src/source-finder/collection-finder.ts +335 -0
- package/src/source-finder/cross-file-tracker.ts +741 -0
- package/src/source-finder/element-finder.ts +387 -0
- package/src/source-finder/image-finder.ts +283 -0
- package/src/source-finder/index.ts +37 -0
- package/src/source-finder/search-index.ts +525 -0
- package/src/source-finder/snippet-utils.ts +668 -0
- package/src/source-finder/source-lookup.ts +200 -0
- package/src/source-finder/types.ts +210 -0
- package/src/source-finder/variable-extraction.ts +406 -0
- package/src/tailwind-colors.ts +874 -0
- package/src/tsconfig.json +25 -0
- package/src/types.ts +406 -0
- package/src/utils.ts +186 -0
- package/src/vite-plugin.ts +42 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { getProjectRoot } from '../config'
|
|
5
|
+
import type { Attribute, ManifestEntry } from '../types'
|
|
6
|
+
import { escapeRegex, generateSourceHash } from '../utils'
|
|
7
|
+
import { buildDefinitionPath } from './ast-extractors'
|
|
8
|
+
import { getCachedParsedFile } from './ast-parser'
|
|
9
|
+
import { findAttributeSourceLocation } from './cross-file-tracker'
|
|
10
|
+
import { findImageElementNearLine, findImageSourceLocation } from './image-finder'
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Text Normalization
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalize text for comparison (handles escaping and entities)
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeText(text: string): string {
|
|
20
|
+
return text
|
|
21
|
+
.trim()
|
|
22
|
+
.replace(/\\'/g, "'") // Escaped single quotes
|
|
23
|
+
.replace(/\\"/g, '"') // Escaped double quotes
|
|
24
|
+
.replace(/'/g, "'") // HTML entity for apostrophe
|
|
25
|
+
.replace(/"/g, '"') // HTML entity for quote
|
|
26
|
+
.replace(/'/g, "'") // HTML entity for apostrophe (alternative)
|
|
27
|
+
.replace(/&/g, '&') // HTML entity for ampersand
|
|
28
|
+
.replace(/ /gi, ' ') // HTML entity for non-breaking space
|
|
29
|
+
.replace(/<br\s*\/?>/gi, '\n') // Normalize <br> tags to newlines
|
|
30
|
+
.replace(/<wbr\s*\/?>/gi, '') // Strip <wbr> tags (word break opportunity, no visible content)
|
|
31
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Strip markdown syntax for text comparison
|
|
37
|
+
*/
|
|
38
|
+
export function stripMarkdownSyntax(text: string): string {
|
|
39
|
+
return text
|
|
40
|
+
.replace(/^#+\s+/, '') // Headers
|
|
41
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1') // Bold
|
|
42
|
+
.replace(/\*([^*]+)\*/g, '$1') // Italic
|
|
43
|
+
.replace(/__([^_]+)__/g, '$1') // Bold (underscore)
|
|
44
|
+
.replace(/_([^_]+)_/g, '$1') // Italic (underscore)
|
|
45
|
+
.replace(/`([^`]+)`/g, '$1') // Inline code
|
|
46
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Links
|
|
47
|
+
.replace(/^\s*[-*+]\s+/, '') // List items
|
|
48
|
+
.replace(/^\s*\d+\.\s+/, '') // Numbered lists
|
|
49
|
+
.trim()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Find the 1-indexed line number where a text value is defined as a string literal.
|
|
54
|
+
* Searches for the text inside quote delimiters ("text", 'text', or `text`).
|
|
55
|
+
* Returns the line number, or undefined if not found.
|
|
56
|
+
*/
|
|
57
|
+
export function findTextDefinitionLine(
|
|
58
|
+
content: string,
|
|
59
|
+
lines: string[],
|
|
60
|
+
text: string,
|
|
61
|
+
): number | undefined {
|
|
62
|
+
// Search for the text inside string delimiters
|
|
63
|
+
for (const quote of ['"', "'", '`']) {
|
|
64
|
+
const searchStr = `${quote}${text}${quote}`
|
|
65
|
+
const idx = content.indexOf(searchStr)
|
|
66
|
+
if (idx !== -1) {
|
|
67
|
+
return content.substring(0, idx).split('\n').length
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Also try with common escape sequences (e.g., escaped quotes within the text)
|
|
72
|
+
const escapedForDouble = text.replace(/"/g, '\\"')
|
|
73
|
+
if (escapedForDouble !== text) {
|
|
74
|
+
const idx = content.indexOf(`"${escapedForDouble}"`)
|
|
75
|
+
if (idx !== -1) {
|
|
76
|
+
return content.substring(0, idx).split('\n').length
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Snippet Extraction
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract complete tag snippet including content and indentation.
|
|
89
|
+
* Exported for use in html-processor to populate sourceSnippet.
|
|
90
|
+
*
|
|
91
|
+
* When startLine points to a line inside the element (e.g., the text content line),
|
|
92
|
+
* this function searches backwards to find the opening tag first.
|
|
93
|
+
*/
|
|
94
|
+
export function extractCompleteTagSnippet(lines: string[], startLine: number, tag: string): string {
|
|
95
|
+
// Pattern to match opening tag - either followed by whitespace/>, or at end of line (multi-line tag)
|
|
96
|
+
const escapedTag = escapeRegex(tag)
|
|
97
|
+
const openTagPattern = new RegExp(`<${escapedTag}(?:[\\s>]|$)`, 'gi')
|
|
98
|
+
|
|
99
|
+
// Check if the start line contains the opening tag
|
|
100
|
+
let actualStartLine = startLine
|
|
101
|
+
const startLineContent = lines[startLine] || ''
|
|
102
|
+
if (!openTagPattern.test(startLineContent)) {
|
|
103
|
+
// Search backwards to find the opening tag
|
|
104
|
+
for (let i = startLine - 1; i >= Math.max(0, startLine - 20); i--) {
|
|
105
|
+
const line = lines[i]
|
|
106
|
+
if (!line) continue
|
|
107
|
+
|
|
108
|
+
// Reset regex lastIndex for fresh test
|
|
109
|
+
openTagPattern.lastIndex = 0
|
|
110
|
+
if (openTagPattern.test(line)) {
|
|
111
|
+
actualStartLine = i
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const snippetLines: string[] = []
|
|
118
|
+
let depth = 0
|
|
119
|
+
let foundClosing = false
|
|
120
|
+
|
|
121
|
+
// Start from the opening tag line
|
|
122
|
+
for (let i = actualStartLine; i < Math.min(actualStartLine + 30, lines.length); i++) {
|
|
123
|
+
const line = lines[i]
|
|
124
|
+
|
|
125
|
+
if (!line) {
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
snippetLines.push(line)
|
|
130
|
+
|
|
131
|
+
// Count opening and closing tags
|
|
132
|
+
// Opening tag can be followed by whitespace, >, or end of line (multi-line tag)
|
|
133
|
+
const openTags = (line.match(new RegExp(`<${escapedTag}(?:[\\s>]|$)`, 'gi')) || []).length
|
|
134
|
+
const selfClosing = (line.match(new RegExp(`<${escapedTag}[^>]*/>`, 'gi')) || []).length
|
|
135
|
+
const closeTags = (line.match(new RegExp(`</${escapedTag}>`, 'gi')) || []).length
|
|
136
|
+
|
|
137
|
+
depth += openTags - selfClosing - closeTags
|
|
138
|
+
|
|
139
|
+
// If we found a self-closing tag or closed all tags, we're done
|
|
140
|
+
if (selfClosing > 0 || (depth <= 0 && (closeTags > 0 || openTags > 0))) {
|
|
141
|
+
foundClosing = true
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If we didn't find closing tag, just return the first line
|
|
147
|
+
if (!foundClosing && snippetLines.length > 1) {
|
|
148
|
+
return snippetLines[0]!
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return snippetLines.join('\n')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Extract just the opening tag from source lines (e.g., `<a href="/foo" class="btn">`)
|
|
156
|
+
* Handles multi-line opening tags.
|
|
157
|
+
*
|
|
158
|
+
* @param lines - Source file lines
|
|
159
|
+
* @param startLine - 0-indexed line number where element starts
|
|
160
|
+
* @param tag - The tag name
|
|
161
|
+
* @returns The opening tag string, or undefined if can't extract
|
|
162
|
+
*/
|
|
163
|
+
export function extractOpeningTagSnippet(lines: string[], startLine: number, tag: string): string | undefined {
|
|
164
|
+
const result = extractOpeningTagWithLine(lines, startLine, tag)
|
|
165
|
+
return result?.snippet
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract the opening tag from source lines along with its starting line number.
|
|
170
|
+
* Handles multi-line opening tags.
|
|
171
|
+
*
|
|
172
|
+
* @param lines - Source file lines
|
|
173
|
+
* @param startLine - 0-indexed line number where element starts
|
|
174
|
+
* @param tag - The tag name
|
|
175
|
+
* @returns Object with the opening tag snippet and 0-indexed startLine, or undefined if can't extract
|
|
176
|
+
*/
|
|
177
|
+
export function extractOpeningTagWithLine(
|
|
178
|
+
lines: string[],
|
|
179
|
+
startLine: number,
|
|
180
|
+
tag: string,
|
|
181
|
+
): { snippet: string; startLine: number } | undefined {
|
|
182
|
+
const escapedTag = escapeRegex(tag)
|
|
183
|
+
const openTagPattern = new RegExp(`<${escapedTag}(?:[\\s>]|$)`, 'gi')
|
|
184
|
+
|
|
185
|
+
// Find the line containing the opening tag
|
|
186
|
+
let actualStartLine = startLine
|
|
187
|
+
const startLineContent = lines[startLine] || ''
|
|
188
|
+
if (!openTagPattern.test(startLineContent)) {
|
|
189
|
+
for (let i = startLine - 1; i >= Math.max(0, startLine - 20); i--) {
|
|
190
|
+
const line = lines[i]
|
|
191
|
+
if (!line) continue
|
|
192
|
+
openTagPattern.lastIndex = 0
|
|
193
|
+
if (openTagPattern.test(line)) {
|
|
194
|
+
actualStartLine = i
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Collect lines until we find the closing > of the opening tag
|
|
201
|
+
const snippetLines: string[] = []
|
|
202
|
+
for (let i = actualStartLine; i < Math.min(actualStartLine + 10, lines.length); i++) {
|
|
203
|
+
const line = lines[i]
|
|
204
|
+
if (!line) continue
|
|
205
|
+
|
|
206
|
+
snippetLines.push(line)
|
|
207
|
+
const combined = snippetLines.join('\n')
|
|
208
|
+
|
|
209
|
+
// Check if we have the complete opening tag (found the closing >)
|
|
210
|
+
// Match from <tag to the first > that's not part of => or />
|
|
211
|
+
const openTagMatch = combined.match(new RegExp(`<${escapedTag}[^>]*>`, 'i'))
|
|
212
|
+
if (openTagMatch) {
|
|
213
|
+
return { snippet: openTagMatch[0], startLine: actualStartLine }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Also check for self-closing tag
|
|
217
|
+
const selfClosingMatch = combined.match(new RegExp(`<${escapedTag}[^>]*/\\s*>`, 'i'))
|
|
218
|
+
if (selfClosingMatch) {
|
|
219
|
+
return { snippet: selfClosingMatch[0], startLine: actualStartLine }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Update attribute source information from an opening tag snippet.
|
|
228
|
+
* Determines whether each attribute is static (quoted value) or dynamic (expression).
|
|
229
|
+
* - For static attributes: sourcePath/Line/Snippet point to the template file
|
|
230
|
+
* - For dynamic attributes: sourcePath/Line/Snippet point to where the VALUE is defined
|
|
231
|
+
*
|
|
232
|
+
* @param openingTagSnippet - The opening tag string (e.g., `<a href={url} class="btn">`)
|
|
233
|
+
* @param attributes - Existing attributes with resolved values (isStatic will be updated)
|
|
234
|
+
* @param sourceFilePath - The source file path (used for static attrs and as starting point for dynamic attr tracing)
|
|
235
|
+
* @param openingTagStartLine - 1-indexed line number where the opening tag starts in the source file
|
|
236
|
+
* @returns Updated attributes with sourcePath, sourceLine, and sourceSnippet
|
|
237
|
+
*/
|
|
238
|
+
export async function updateAttributeSources(
|
|
239
|
+
openingTagSnippet: string,
|
|
240
|
+
attributes: Record<string, Attribute>,
|
|
241
|
+
sourceFilePath?: string,
|
|
242
|
+
openingTagStartLine?: number,
|
|
243
|
+
sourceLines?: string[],
|
|
244
|
+
): Promise<Record<string, Attribute>> {
|
|
245
|
+
const result: Record<string, Attribute> = {}
|
|
246
|
+
|
|
247
|
+
// Normalize the snippet (remove newlines, collapse whitespace for easier parsing)
|
|
248
|
+
const normalized = openingTagSnippet.replace(/\s+/g, ' ')
|
|
249
|
+
|
|
250
|
+
// Split opening tag into lines for finding attribute line numbers
|
|
251
|
+
const snippetLines = openingTagSnippet.split('\n')
|
|
252
|
+
|
|
253
|
+
// Process each attribute
|
|
254
|
+
const attrPromises = Object.entries(attributes).map(async ([attrName, attr]) => {
|
|
255
|
+
const { value } = attr
|
|
256
|
+
|
|
257
|
+
// Check for expression attribute: attr={expression} or attr={`template`}
|
|
258
|
+
const escapedAttrName = escapeRegex(attrName)
|
|
259
|
+
const exprPattern = new RegExp(`${escapedAttrName}\\s*=\\s*\\{([^}]+)\\}`, 'i')
|
|
260
|
+
const exprMatch = normalized.match(exprPattern)
|
|
261
|
+
|
|
262
|
+
if (exprMatch) {
|
|
263
|
+
const expression = exprMatch[1]!.trim()
|
|
264
|
+
const isTemplateLiteral = expression.startsWith('`') && expression.endsWith('`')
|
|
265
|
+
const cleanExpression = isTemplateLiteral ? expression.slice(1, -1) : expression
|
|
266
|
+
|
|
267
|
+
// For dynamic attributes, search by VALUE to find the source definition
|
|
268
|
+
if (sourceFilePath) {
|
|
269
|
+
const sourceLocation = await findAttributeSourceLocation(cleanExpression, value, sourceFilePath)
|
|
270
|
+
if (sourceLocation) {
|
|
271
|
+
return [attrName, {
|
|
272
|
+
value,
|
|
273
|
+
sourcePath: sourceLocation.file,
|
|
274
|
+
sourceLine: sourceLocation.line,
|
|
275
|
+
sourceSnippet: sourceLocation.snippet,
|
|
276
|
+
}] as const
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Couldn't resolve - return without source info
|
|
281
|
+
return [attrName, { value }] as const
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check for static attribute: attr="value" or attr='value'
|
|
285
|
+
const staticPattern = new RegExp(`${escapedAttrName}\\s*=\\s*["']([^"']*)["']`, 'i')
|
|
286
|
+
const staticMatch = normalized.match(staticPattern)
|
|
287
|
+
|
|
288
|
+
if (staticMatch) {
|
|
289
|
+
const attrLine = findAttributeLineInSnippet(attrName, snippetLines, openingTagStartLine)
|
|
290
|
+
|
|
291
|
+
return [attrName, {
|
|
292
|
+
value,
|
|
293
|
+
sourcePath: sourceFilePath,
|
|
294
|
+
sourceLine: attrLine,
|
|
295
|
+
sourceSnippet: (attrLine && sourceLines) ? sourceLines[attrLine - 1] || '' : undefined,
|
|
296
|
+
}] as const
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Check for boolean attribute (just the attribute name, no value)
|
|
300
|
+
const boolPattern = new RegExp(`\\s${escapedAttrName}(?:\\s|>|/>)`, 'i')
|
|
301
|
+
if (boolPattern.test(normalized)) {
|
|
302
|
+
const attrLine = findAttributeLineInSnippet(attrName, snippetLines, openingTagStartLine)
|
|
303
|
+
|
|
304
|
+
return [attrName, {
|
|
305
|
+
value,
|
|
306
|
+
sourcePath: sourceFilePath,
|
|
307
|
+
sourceLine: attrLine,
|
|
308
|
+
sourceSnippet: (attrLine && sourceLines) ? sourceLines[attrLine - 1] || '' : undefined,
|
|
309
|
+
}] as const
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Fallback: couldn't determine source type, keep original
|
|
313
|
+
return [attrName, attr] as const
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
const results = await Promise.all(attrPromises)
|
|
317
|
+
for (const [attrName, attrValue] of results) {
|
|
318
|
+
result[attrName] = attrValue
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return result
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Find the 1-indexed line number of an attribute within an opening tag snippet.
|
|
326
|
+
*/
|
|
327
|
+
function findAttributeLineInSnippet(
|
|
328
|
+
attrName: string,
|
|
329
|
+
snippetLines: string[],
|
|
330
|
+
startLine?: number,
|
|
331
|
+
): number | undefined {
|
|
332
|
+
if (!startLine) return undefined
|
|
333
|
+
const attrPattern = new RegExp(`(?:^|\\s)${escapeRegex(attrName)}(?:\\s*=|\\s|>|/>|$)`, 'i')
|
|
334
|
+
for (let i = 0; i < snippetLines.length; i++) {
|
|
335
|
+
if (attrPattern.test(snippetLines[i]!)) {
|
|
336
|
+
return startLine + i
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return undefined
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Update colorClasses entries with source info from the class attribute in the opening tag.
|
|
344
|
+
* All color classes come from the same `class="..."` attribute, so they share the same source location.
|
|
345
|
+
*/
|
|
346
|
+
export function updateColorClassSources(
|
|
347
|
+
openingTagSnippet: string,
|
|
348
|
+
colorClasses: Record<string, Attribute>,
|
|
349
|
+
sourceFilePath?: string,
|
|
350
|
+
openingTagStartLine?: number,
|
|
351
|
+
sourceLines?: string[],
|
|
352
|
+
): Record<string, Attribute> {
|
|
353
|
+
const snippetLines = openingTagSnippet.split('\n')
|
|
354
|
+
const classLine = findAttributeLineInSnippet('class', snippetLines, openingTagStartLine)
|
|
355
|
+
const sourceSnippet = (classLine && sourceLines) ? sourceLines[classLine - 1] || '' : undefined
|
|
356
|
+
|
|
357
|
+
const result: Record<string, Attribute> = {}
|
|
358
|
+
for (const [key, attr] of Object.entries(colorClasses)) {
|
|
359
|
+
result[key] = {
|
|
360
|
+
...attr,
|
|
361
|
+
sourcePath: sourceFilePath,
|
|
362
|
+
sourceLine: classLine,
|
|
363
|
+
sourceSnippet,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return result
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Extract innerHTML from a complete tag snippet.
|
|
371
|
+
* Given `<p class="foo">content here</p>`, returns `content here`.
|
|
372
|
+
*
|
|
373
|
+
* @param snippet - The complete tag snippet from source
|
|
374
|
+
* @param tag - The tag name (e.g., 'p', 'h1')
|
|
375
|
+
* @returns The innerHTML portion, or undefined if can't extract
|
|
376
|
+
*/
|
|
377
|
+
export function extractInnerHtmlFromSnippet(snippet: string, tag: string): string | undefined {
|
|
378
|
+
// Match opening tag (with any attributes) and extract content until closing tag
|
|
379
|
+
// Handle both single-line and multi-line cases
|
|
380
|
+
const escapedTag = escapeRegex(tag)
|
|
381
|
+
const openTagPattern = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>`, 'i')
|
|
382
|
+
const closeTagPattern = new RegExp(`</${escapedTag}>`, 'i')
|
|
383
|
+
|
|
384
|
+
const openMatch = snippet.match(openTagPattern)
|
|
385
|
+
if (!openMatch) return undefined
|
|
386
|
+
|
|
387
|
+
const openTagEnd = openMatch.index! + openMatch[0].length
|
|
388
|
+
const closeMatch = snippet.match(closeTagPattern)
|
|
389
|
+
if (!closeMatch) return undefined
|
|
390
|
+
|
|
391
|
+
const closeTagStart = closeMatch.index!
|
|
392
|
+
|
|
393
|
+
// Extract content between opening and closing tags
|
|
394
|
+
if (closeTagStart > openTagEnd) {
|
|
395
|
+
return snippet.substring(openTagEnd, closeTagStart)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return undefined
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Extract the full <img> tag snippet from source lines
|
|
403
|
+
*/
|
|
404
|
+
export function extractImageSnippet(lines: string[], startLine: number): string {
|
|
405
|
+
const snippetLines: string[] = []
|
|
406
|
+
let foundClosing = false
|
|
407
|
+
|
|
408
|
+
for (let i = startLine; i < Math.min(startLine + 10, lines.length); i++) {
|
|
409
|
+
const line = lines[i]
|
|
410
|
+
if (!line) continue
|
|
411
|
+
|
|
412
|
+
snippetLines.push(line)
|
|
413
|
+
|
|
414
|
+
// Check if this line contains the closing of the img tag
|
|
415
|
+
// img tags can be self-closing /> or just >
|
|
416
|
+
if (line.includes('/>') || (line.includes('<img') && line.includes('>'))) {
|
|
417
|
+
foundClosing = true
|
|
418
|
+
break
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!foundClosing && snippetLines.length > 1) {
|
|
423
|
+
return snippetLines[0]!
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return snippetLines.join('\n')
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Read source file and extract the complete element at the specified line.
|
|
431
|
+
*
|
|
432
|
+
* @param sourceFile - Path to source file (relative to cwd)
|
|
433
|
+
* @param sourceLine - 1-indexed line number
|
|
434
|
+
* @param tag - The tag name
|
|
435
|
+
* @returns The complete element from source, or undefined if can't extract
|
|
436
|
+
*/
|
|
437
|
+
export async function extractSourceSnippet(
|
|
438
|
+
sourceFile: string,
|
|
439
|
+
sourceLine: number,
|
|
440
|
+
tag: string,
|
|
441
|
+
): Promise<string | undefined> {
|
|
442
|
+
try {
|
|
443
|
+
const filePath = path.isAbsolute(sourceFile)
|
|
444
|
+
? sourceFile
|
|
445
|
+
: path.join(getProjectRoot(), sourceFile)
|
|
446
|
+
|
|
447
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
448
|
+
const lines = content.split('\n')
|
|
449
|
+
|
|
450
|
+
// Extract the complete tag snippet (including wrapper element)
|
|
451
|
+
return extractCompleteTagSnippet(lines, sourceLine - 1, tag)
|
|
452
|
+
} catch {
|
|
453
|
+
return undefined
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ============================================================================
|
|
458
|
+
// Manifest Enhancement
|
|
459
|
+
// ============================================================================
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Enhance manifest entries with actual source snippets from source files.
|
|
463
|
+
* This reads the source files and extracts the innerHTML at the specified locations.
|
|
464
|
+
* For images, it finds the correct line containing the src attribute.
|
|
465
|
+
*
|
|
466
|
+
* @param entries - Manifest entries to enhance
|
|
467
|
+
* @returns Enhanced entries with sourceSnippet and openingTagSnippet populated
|
|
468
|
+
*/
|
|
469
|
+
export async function enhanceManifestWithSourceSnippets(
|
|
470
|
+
entries: Record<string, ManifestEntry>,
|
|
471
|
+
): Promise<Record<string, ManifestEntry>> {
|
|
472
|
+
const enhanced: Record<string, ManifestEntry> = {}
|
|
473
|
+
|
|
474
|
+
// Process entries in parallel for better performance
|
|
475
|
+
const entryPromises = Object.entries(entries).map(async ([id, entry]) => {
|
|
476
|
+
// Handle image entries specially - find the line with src attribute
|
|
477
|
+
if (entry.imageMetadata?.src) {
|
|
478
|
+
const imageLocation = await findImageSourceLocation(entry.imageMetadata.src, entry.imageMetadata.srcSet)
|
|
479
|
+
if (imageLocation) {
|
|
480
|
+
const sourceHash = generateSourceHash(imageLocation.snippet || entry.imageMetadata.src)
|
|
481
|
+
const updated: ManifestEntry = {
|
|
482
|
+
...entry,
|
|
483
|
+
sourcePath: imageLocation.file,
|
|
484
|
+
sourceLine: imageLocation.line,
|
|
485
|
+
sourceSnippet: imageLocation.snippet,
|
|
486
|
+
sourceHash,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Also update attribute and colorClasses source info from the opening tag
|
|
490
|
+
try {
|
|
491
|
+
const filePath = path.isAbsolute(imageLocation.file)
|
|
492
|
+
? imageLocation.file
|
|
493
|
+
: path.join(getProjectRoot(), imageLocation.file)
|
|
494
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
495
|
+
const lines = content.split('\n')
|
|
496
|
+
const openingTagInfo = extractOpeningTagWithLine(lines, imageLocation.line - 1, entry.tag)
|
|
497
|
+
|
|
498
|
+
if (openingTagInfo) {
|
|
499
|
+
const startLine = openingTagInfo.startLine + 1
|
|
500
|
+
if (updated.attributes) {
|
|
501
|
+
updated.attributes = await updateAttributeSources(
|
|
502
|
+
openingTagInfo.snippet,
|
|
503
|
+
updated.attributes,
|
|
504
|
+
imageLocation.file,
|
|
505
|
+
startLine,
|
|
506
|
+
lines,
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
if (updated.colorClasses) {
|
|
510
|
+
updated.colorClasses = updateColorClassSources(
|
|
511
|
+
openingTagInfo.snippet,
|
|
512
|
+
updated.colorClasses,
|
|
513
|
+
imageLocation.file,
|
|
514
|
+
startLine,
|
|
515
|
+
lines,
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} catch {
|
|
520
|
+
// Couldn't read file - return without source lines on attributes
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return [id, updated] as const
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Fallback for expression-based src attributes (src={variable})
|
|
527
|
+
// Use the entry's existing sourcePath/sourceLine to find the img tag
|
|
528
|
+
// by its position in the AST rather than by src value
|
|
529
|
+
if (entry.sourcePath && entry.sourceLine) {
|
|
530
|
+
try {
|
|
531
|
+
const filePath = path.isAbsolute(entry.sourcePath)
|
|
532
|
+
? entry.sourcePath
|
|
533
|
+
: path.join(getProjectRoot(), entry.sourcePath)
|
|
534
|
+
const cached = await getCachedParsedFile(filePath)
|
|
535
|
+
if (cached) {
|
|
536
|
+
const nearbyImg = findImageElementNearLine(cached.ast, entry.sourceLine, cached.lines)
|
|
537
|
+
if (nearbyImg) {
|
|
538
|
+
const sourceHash = generateSourceHash(nearbyImg.snippet || entry.imageMetadata.src)
|
|
539
|
+
return [id, {
|
|
540
|
+
...entry,
|
|
541
|
+
sourceLine: nearbyImg.line,
|
|
542
|
+
sourceSnippet: nearbyImg.snippet,
|
|
543
|
+
sourceHash,
|
|
544
|
+
}] as const
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} catch {
|
|
548
|
+
// Fallback search failed
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return [id, entry] as const
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Skip if already has sourceSnippet or missing source info
|
|
556
|
+
if (entry.sourceSnippet || !entry.sourcePath || !entry.sourceLine || !entry.tag) {
|
|
557
|
+
return [id, entry] as const
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Read file once and extract both snippets
|
|
561
|
+
try {
|
|
562
|
+
const filePath = path.isAbsolute(entry.sourcePath)
|
|
563
|
+
? entry.sourcePath
|
|
564
|
+
: path.join(getProjectRoot(), entry.sourcePath)
|
|
565
|
+
|
|
566
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
567
|
+
const lines = content.split('\n')
|
|
568
|
+
|
|
569
|
+
// Extract the complete source element
|
|
570
|
+
const sourceSnippet = extractCompleteTagSnippet(lines, entry.sourceLine - 1, entry.tag)
|
|
571
|
+
|
|
572
|
+
// Extract opening tag with its start line for attribute line tracking
|
|
573
|
+
const openingTagInfo = extractOpeningTagWithLine(lines, entry.sourceLine - 1, entry.tag)
|
|
574
|
+
|
|
575
|
+
// Update attribute sources if we have an opening tag and attributes
|
|
576
|
+
// - Static attributes get sourceLine/snippet from the template
|
|
577
|
+
// - Dynamic attributes get traced to their actual value definition
|
|
578
|
+
let attributes = entry.attributes
|
|
579
|
+
if (openingTagInfo && attributes) {
|
|
580
|
+
attributes = await updateAttributeSources(
|
|
581
|
+
openingTagInfo.snippet,
|
|
582
|
+
attributes,
|
|
583
|
+
entry.sourcePath,
|
|
584
|
+
openingTagInfo.startLine + 1, // Convert to 1-indexed
|
|
585
|
+
lines,
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Update colorClasses with source info from the class attribute
|
|
590
|
+
let colorClasses = entry.colorClasses
|
|
591
|
+
if (openingTagInfo && colorClasses) {
|
|
592
|
+
colorClasses = updateColorClassSources(
|
|
593
|
+
openingTagInfo.snippet,
|
|
594
|
+
colorClasses,
|
|
595
|
+
entry.sourcePath,
|
|
596
|
+
openingTagInfo.startLine + 1, // Convert to 1-indexed
|
|
597
|
+
lines,
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (sourceSnippet) {
|
|
602
|
+
const trimmedText = entry.text?.trim()
|
|
603
|
+
|
|
604
|
+
// Check if text is directly in the snippet (static content)
|
|
605
|
+
if (trimmedText && !sourceSnippet.includes(trimmedText)) {
|
|
606
|
+
// Text from dynamic expression — resolve via variable definitions
|
|
607
|
+
const cached = await getCachedParsedFile(filePath)
|
|
608
|
+
if (cached) {
|
|
609
|
+
const normalizedSearch = normalizeText(entry.text!)
|
|
610
|
+
const matchingDef = cached.variableDefinitions.find(
|
|
611
|
+
def => normalizeText(def.value) === normalizedSearch,
|
|
612
|
+
)
|
|
613
|
+
if (matchingDef) {
|
|
614
|
+
const defSnippet = lines[matchingDef.line - 1] || ''
|
|
615
|
+
const sourceHash = generateSourceHash(defSnippet)
|
|
616
|
+
return [id, {
|
|
617
|
+
...entry,
|
|
618
|
+
sourceLine: matchingDef.line,
|
|
619
|
+
sourceSnippet: defSnippet,
|
|
620
|
+
variableName: buildDefinitionPath(matchingDef),
|
|
621
|
+
attributes,
|
|
622
|
+
colorClasses,
|
|
623
|
+
sourceHash,
|
|
624
|
+
}] as const
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Fallback: search for the literal text in file content
|
|
629
|
+
// This handles cases where AST-based lookup fails (e.g., concurrent parsing)
|
|
630
|
+
const foundLine = findTextDefinitionLine(content, lines, trimmedText)
|
|
631
|
+
if (foundLine) {
|
|
632
|
+
const defSnippet = lines[foundLine - 1] || ''
|
|
633
|
+
const sourceHash = generateSourceHash(defSnippet)
|
|
634
|
+
return [id, {
|
|
635
|
+
...entry,
|
|
636
|
+
sourceLine: foundLine,
|
|
637
|
+
sourceSnippet: defSnippet,
|
|
638
|
+
attributes,
|
|
639
|
+
colorClasses,
|
|
640
|
+
sourceHash,
|
|
641
|
+
}] as const
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Original static content path
|
|
646
|
+
const sourceHash = generateSourceHash(sourceSnippet)
|
|
647
|
+
return [id, {
|
|
648
|
+
...entry,
|
|
649
|
+
sourceSnippet,
|
|
650
|
+
attributes,
|
|
651
|
+
colorClasses,
|
|
652
|
+
sourceHash,
|
|
653
|
+
}] as const
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
// Fall through to return entry as-is
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return [id, entry] as const
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
const results = await Promise.all(entryPromises)
|
|
663
|
+
for (const [id, entry] of results) {
|
|
664
|
+
enhanced[id] = entry
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return enhanced
|
|
668
|
+
}
|