@nuasite/cms 0.37.0 → 0.38.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/dist/editor.js +6119 -6090
- package/package.json +1 -1
- package/src/editor/components/editable-highlights.tsx +18 -56
- package/src/editor/dom.ts +37 -0
- package/src/editor/editor.ts +42 -1
- package/src/source-finder/cross-file-tracker.ts +16 -6
- package/src/source-finder/element-finder.ts +135 -3
- package/src/source-finder/search-index.ts +319 -98
- package/src/source-finder/snippet-utils.ts +45 -42
- package/src/source-finder/source-lookup.ts +5 -2
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
findInTextIndex,
|
|
23
23
|
findTemplateElementUsingStringLiteral,
|
|
24
24
|
findTranslationByKeyAndText,
|
|
25
|
+
findVariableHitInFile,
|
|
25
26
|
initializeSearchIndex,
|
|
26
27
|
isTranslationFilePath,
|
|
27
28
|
} from './search-index'
|
|
@@ -110,20 +111,19 @@ export function findTextDefinitionLine(
|
|
|
110
111
|
* this function searches backwards to find the opening tag first.
|
|
111
112
|
*/
|
|
112
113
|
export function extractCompleteTagSnippet(lines: string[], startLine: number, tag: string): string {
|
|
113
|
-
// Pattern to match opening tag - either followed by whitespace/>, or at end of line (multi-line tag)
|
|
114
114
|
const escapedTag = escapeRegex(tag)
|
|
115
|
+
// Opening tag — followed by whitespace/`>`, or at end of line (multi-line tag).
|
|
115
116
|
const openTagPattern = new RegExp(`<${escapedTag}(?:[\\s>]|$)`, 'gi')
|
|
117
|
+
const selfClosingPattern = new RegExp(`<${escapedTag}[^>]*/>`, 'gi')
|
|
118
|
+
const closeTagPattern = new RegExp(`</${escapedTag}>`, 'gi')
|
|
116
119
|
|
|
117
|
-
// Check if the start line contains the opening tag
|
|
118
120
|
let actualStartLine = startLine
|
|
119
121
|
const startLineContent = lines[startLine] || ''
|
|
120
122
|
if (!openTagPattern.test(startLineContent)) {
|
|
121
|
-
// Search backwards
|
|
123
|
+
// Search backwards for the opening tag.
|
|
122
124
|
for (let i = startLine - 1; i >= Math.max(0, startLine - 20); i--) {
|
|
123
125
|
const line = lines[i]
|
|
124
126
|
if (!line) continue
|
|
125
|
-
|
|
126
|
-
// Reset regex lastIndex for fresh test
|
|
127
127
|
openTagPattern.lastIndex = 0
|
|
128
128
|
if (openTagPattern.test(line)) {
|
|
129
129
|
actualStartLine = i
|
|
@@ -136,32 +136,27 @@ export function extractCompleteTagSnippet(lines: string[], startLine: number, ta
|
|
|
136
136
|
let depth = 0
|
|
137
137
|
let foundClosing = false
|
|
138
138
|
|
|
139
|
-
// Start from the opening tag line
|
|
140
139
|
for (let i = actualStartLine; i < Math.min(actualStartLine + 30, lines.length); i++) {
|
|
141
|
-
const line = lines[i]
|
|
142
|
-
|
|
143
|
-
if (!line) {
|
|
144
|
-
continue
|
|
145
|
-
}
|
|
140
|
+
const line = lines[i] ?? ''
|
|
146
141
|
|
|
142
|
+
// Preserve blank lines verbatim — the snippet must match the file byte-for-byte
|
|
143
|
+
// so the writer's `content.includes(sourceSnippet)` check passes. Blank lines
|
|
144
|
+
// are common between frontmatter and the template body.
|
|
147
145
|
snippetLines.push(line)
|
|
146
|
+
if (!line) continue
|
|
148
147
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
const selfClosing = (line.match(new RegExp(`<${escapedTag}[^>]*/>`, 'gi')) || []).length
|
|
153
|
-
const closeTags = (line.match(new RegExp(`</${escapedTag}>`, 'gi')) || []).length
|
|
148
|
+
const openTags = countMatches(line, openTagPattern)
|
|
149
|
+
const selfClosing = countMatches(line, selfClosingPattern)
|
|
150
|
+
const closeTags = countMatches(line, closeTagPattern)
|
|
154
151
|
|
|
155
152
|
depth += openTags - selfClosing - closeTags
|
|
156
153
|
|
|
157
|
-
// If we found a self-closing tag or closed all tags, we're done
|
|
158
154
|
if (selfClosing > 0 || (depth <= 0 && (closeTags > 0 || openTags > 0))) {
|
|
159
155
|
foundClosing = true
|
|
160
156
|
break
|
|
161
157
|
}
|
|
162
158
|
}
|
|
163
159
|
|
|
164
|
-
// If we didn't find closing tag, just return the first line
|
|
165
160
|
if (!foundClosing && snippetLines.length > 1) {
|
|
166
161
|
return snippetLines[0]!
|
|
167
162
|
}
|
|
@@ -169,6 +164,14 @@ export function extractCompleteTagSnippet(lines: string[], startLine: number, ta
|
|
|
169
164
|
return snippetLines.join('\n')
|
|
170
165
|
}
|
|
171
166
|
|
|
167
|
+
/** Count global-regex matches on a string without allocating the match array. */
|
|
168
|
+
function countMatches(str: string, pattern: RegExp): number {
|
|
169
|
+
pattern.lastIndex = 0
|
|
170
|
+
let count = 0
|
|
171
|
+
while (pattern.exec(str) !== null) count++
|
|
172
|
+
return count
|
|
173
|
+
}
|
|
174
|
+
|
|
172
175
|
/**
|
|
173
176
|
* Extract just the opening tag from source lines (e.g., `<a href="/foo" class="btn">`)
|
|
174
177
|
* Handles multi-line opening tags.
|
|
@@ -199,8 +202,10 @@ export function extractOpeningTagWithLine(
|
|
|
199
202
|
): { snippet: string; startLine: number } | undefined {
|
|
200
203
|
const escapedTag = escapeRegex(tag)
|
|
201
204
|
const openTagPattern = new RegExp(`<${escapedTag}(?:[\\s>]|$)`, 'gi')
|
|
205
|
+
// Match `<tag …>` (the closing > of the opening tag), or `<tag … />` (self-closing).
|
|
206
|
+
const openTagMatcher = new RegExp(`<${escapedTag}[^>]*>`, 'i')
|
|
207
|
+
const selfClosingMatcher = new RegExp(`<${escapedTag}[^>]*/\\s*>`, 'i')
|
|
202
208
|
|
|
203
|
-
// Find the line containing the opening tag
|
|
204
209
|
let actualStartLine = startLine
|
|
205
210
|
const startLineContent = lines[startLine] || ''
|
|
206
211
|
if (!openTagPattern.test(startLineContent)) {
|
|
@@ -215,7 +220,6 @@ export function extractOpeningTagWithLine(
|
|
|
215
220
|
}
|
|
216
221
|
}
|
|
217
222
|
|
|
218
|
-
// Collect lines until we find the closing > of the opening tag
|
|
219
223
|
const snippetLines: string[] = []
|
|
220
224
|
for (let i = actualStartLine; i < Math.min(actualStartLine + 10, lines.length); i++) {
|
|
221
225
|
const line = lines[i]
|
|
@@ -224,15 +228,12 @@ export function extractOpeningTagWithLine(
|
|
|
224
228
|
snippetLines.push(line)
|
|
225
229
|
const combined = snippetLines.join('\n')
|
|
226
230
|
|
|
227
|
-
|
|
228
|
-
// Match from <tag to the first > that's not part of => or />
|
|
229
|
-
const openTagMatch = combined.match(new RegExp(`<${escapedTag}[^>]*>`, 'i'))
|
|
231
|
+
const openTagMatch = combined.match(openTagMatcher)
|
|
230
232
|
if (openTagMatch) {
|
|
231
233
|
return { snippet: openTagMatch[0], startLine: actualStartLine }
|
|
232
234
|
}
|
|
233
235
|
|
|
234
|
-
|
|
235
|
-
const selfClosingMatch = combined.match(new RegExp(`<${escapedTag}[^>]*/\\s*>`, 'i'))
|
|
236
|
+
const selfClosingMatch = combined.match(selfClosingMatcher)
|
|
236
237
|
if (selfClosingMatch) {
|
|
237
238
|
return { snippet: selfClosingMatch[0], startLine: actualStartLine }
|
|
238
239
|
}
|
|
@@ -849,28 +850,30 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
849
850
|
}
|
|
850
851
|
}
|
|
851
852
|
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
// (
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
// `data-astro-source-*` attributes. Only (1) needs the translation
|
|
858
|
-
// treatment (text edits target the JSON, styling disabled); (2) just
|
|
859
|
-
// needs source coordinates so standard snippet extraction below picks
|
|
860
|
-
// up the full element.
|
|
853
|
+
// Two cases want the index over the marking phase:
|
|
854
|
+
// (1) text rendered via translation helpers / runtimes that don't inject
|
|
855
|
+
// `data-astro-source-*` (toast i18n the JSON; rendered template gets coords).
|
|
856
|
+
// (2) `{fact.value}`-style expressions where Astro points sourceLine at the
|
|
857
|
+
// JSX template line — the real edit target is the variable definition.
|
|
861
858
|
const trimmedEntryText = entry.text?.trim()
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
859
|
+
const alreadyResolved = entry.sourceSnippet || entry.variableName
|
|
860
|
+
if (!alreadyResolved && trimmedEntryText && entry.tag) {
|
|
861
|
+
const sameFileHit = entry.sourcePath
|
|
862
|
+
? findVariableHitInFile(trimmedEntryText, entry.tag, entry.sourcePath)
|
|
863
|
+
: undefined
|
|
864
|
+
const noCoords = !entry.sourcePath || !entry.sourceLine
|
|
865
|
+
const winner = sameFileHit ?? (noCoords ? findInTextIndex(trimmedEntryText, entry.tag) : undefined)
|
|
866
|
+
|
|
867
|
+
if (winner) {
|
|
868
|
+
if (isTranslationFilePath(winner.file)) {
|
|
869
|
+
const resolved = await applyTranslationSource(entry, winner, entry.attributes, entry.colorClasses)
|
|
867
870
|
return [id, resolved] as const
|
|
868
871
|
}
|
|
869
872
|
entry = {
|
|
870
873
|
...entry,
|
|
871
|
-
sourcePath:
|
|
872
|
-
sourceLine:
|
|
873
|
-
...(
|
|
874
|
+
sourcePath: winner.file,
|
|
875
|
+
sourceLine: winner.line,
|
|
876
|
+
...(winner.variableName ? { variableName: winner.variableName } : {}),
|
|
874
877
|
}
|
|
875
878
|
}
|
|
876
879
|
}
|
|
@@ -24,10 +24,13 @@ export async function findSourceLocation(
|
|
|
24
24
|
): Promise<SourceLocation | undefined> {
|
|
25
25
|
// Use index if available (much faster)
|
|
26
26
|
if (isSearchIndexInitialized()) {
|
|
27
|
-
|
|
27
|
+
const indexHit = findInTextIndex(textContent, tag)
|
|
28
|
+
if (indexHit) return indexHit
|
|
29
|
+
// Fall through to slow search on miss — covers cases the per-file
|
|
30
|
+
// indexer can't pre-emit, like a child component's `.map()` over a
|
|
31
|
+
// prop array whose source values live in the parent file.
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
// Fallback to slow search if index not initialized
|
|
31
34
|
const srcDir = path.join(getProjectRoot(), 'src')
|
|
32
35
|
|
|
33
36
|
try {
|