@nuasite/cms 0.37.0 → 0.39.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 +11524 -11456
- package/package.json +1 -1
- package/src/dev-middleware.ts +51 -6
- package/src/editor/components/editable-highlights.tsx +18 -56
- package/src/editor/components/markdown-editor-overlay.tsx +2 -0
- package/src/editor/constants.ts +2 -0
- package/src/editor/dom.ts +37 -0
- package/src/editor/editor.ts +66 -8
- package/src/editor/index.tsx +11 -6
- package/src/editor/markdown-api.ts +9 -0
- package/src/editor/signals.ts +113 -7
- package/src/editor/storage.ts +172 -196
- package/src/editor/types.ts +2 -0
- package/src/handlers/api-routes.ts +27 -14
- package/src/handlers/request-utils.ts +10 -1
- package/src/index.ts +21 -10
- 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 +362 -98
- package/src/source-finder/snippet-utils.ts +45 -42
- package/src/source-finder/source-lookup.ts +5 -2
|
@@ -6,10 +6,19 @@ import { escapeRegex, resolveSourcePath } from '../utils'
|
|
|
6
6
|
import { buildDefinitionPath, parseExpressionPath } from './ast-extractors'
|
|
7
7
|
import { getCachedParsedFile } from './ast-parser'
|
|
8
8
|
import { findComponentProp, findExpressionProp, findSpreadProp } from './element-finder'
|
|
9
|
+
import { wildcardPathToRegexBody } from './search-index'
|
|
9
10
|
import { normalizeText } from './snippet-utils'
|
|
10
11
|
import type { ImportInfo, SourceLocation, VariableDefinition } from './types'
|
|
11
12
|
import { getExportedDefinitions, resolveImportPath } from './variable-extraction'
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Compile a wildcard path like `navItems[*].label` into a regex matching
|
|
16
|
+
* concrete definition paths (`navItems[0].label`, `navItems[3].label`, …).
|
|
17
|
+
*/
|
|
18
|
+
function buildWildcardPathRegex(wildcardPath: string): RegExp {
|
|
19
|
+
return new RegExp('^' + wildcardPathToRegexBody(wildcardPath) + '$')
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
// ============================================================================
|
|
14
23
|
// Expression Prop Search
|
|
15
24
|
// ============================================================================
|
|
@@ -104,16 +113,17 @@ async function searchDirForExpressionProp(
|
|
|
104
113
|
if (exprPropMatch) {
|
|
105
114
|
// The expression text might be a simple variable like 'navItems'
|
|
106
115
|
const exprText = exprPropMatch.expressionText
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
116
|
+
// Substitute the parent's local name for the child's:
|
|
117
|
+
// child `items[0]` + parent `navItems` → `navItems[0]`
|
|
118
|
+
// May contain `[*]` wildcards when the child resolved a `.map()` callback
|
|
119
|
+
// param — compare via regex to match concrete indices in the parent.
|
|
111
120
|
const parentPath = expressionPath.replace(/^[^.[]+/, exprText)
|
|
121
|
+
const parentPathRegex = parentPath.includes('[*]') ? buildWildcardPathRegex(parentPath) : null
|
|
112
122
|
|
|
113
|
-
// Check if the value is in local variable definitions
|
|
114
123
|
for (const def of cached.variableDefinitions) {
|
|
115
124
|
const defPath = buildDefinitionPath(def)
|
|
116
|
-
|
|
125
|
+
const matches = parentPathRegex ? parentPathRegex.test(defPath) : defPath === parentPath
|
|
126
|
+
if (matches) {
|
|
117
127
|
const normalizedDef = normalizeText(def.value)
|
|
118
128
|
if (normalizedDef === normalizedSearch) {
|
|
119
129
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComponentNode, ElementNode, Node as AstroNode, TextNode } from '@astrojs/compiler/types'
|
|
2
2
|
|
|
3
3
|
import { buildDefinitionPath, parseExpressionPath } from './ast-extractors'
|
|
4
|
+
import { makeLeafPathRegex, resolveMapChain } from './search-index'
|
|
4
5
|
import { normalizeText } from './snippet-utils'
|
|
5
6
|
import type {
|
|
6
7
|
ComponentPropMatch,
|
|
@@ -100,7 +101,9 @@ export function findElementWithText(
|
|
|
100
101
|
return match?.[1] ?? exprPath
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
function visit(node: AstroNode) {
|
|
104
|
+
function visit(node: AstroNode, parentExpression: AstroNode | null) {
|
|
105
|
+
const currentExpr = node.type === 'expression' ? node : parentExpression
|
|
106
|
+
|
|
104
107
|
// Check if this is an element or component matching our tag
|
|
105
108
|
if ((node.type === 'element' || node.type === 'component') && node.name.toLowerCase() === tagLower) {
|
|
106
109
|
const elemNode = node as ElementNode | ComponentNode
|
|
@@ -166,6 +169,43 @@ export function findElementWithText(
|
|
|
166
169
|
importInfo,
|
|
167
170
|
expressionPath: exprPath,
|
|
168
171
|
})
|
|
172
|
+
} else if (currentExpr) {
|
|
173
|
+
// `baseVar` may be a `.map()` callback parameter or destructured
|
|
174
|
+
// property. Resolve it to the source array; if the array is local
|
|
175
|
+
// take the leaf, otherwise surface as a cross-file prop candidate.
|
|
176
|
+
const mapMatch = resolveMapForLocal(
|
|
177
|
+
exprPath,
|
|
178
|
+
currentExpr,
|
|
179
|
+
variableDefinitions,
|
|
180
|
+
normalizedSearch,
|
|
181
|
+
)
|
|
182
|
+
if (mapMatch && bestScore < 100) {
|
|
183
|
+
bestScore = 100
|
|
184
|
+
bestMatch = {
|
|
185
|
+
line,
|
|
186
|
+
type: 'variable',
|
|
187
|
+
variableName: mapMatch.variableName,
|
|
188
|
+
definitionLine: mapMatch.definitionLine,
|
|
189
|
+
}
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!mapMatch) {
|
|
194
|
+
const mapPropCandidate = resolveMapForProp(
|
|
195
|
+
exprPath,
|
|
196
|
+
currentExpr,
|
|
197
|
+
propAliases,
|
|
198
|
+
)
|
|
199
|
+
if (mapPropCandidate) {
|
|
200
|
+
propCandidates.push({
|
|
201
|
+
line,
|
|
202
|
+
type: 'variable',
|
|
203
|
+
usesProp: true,
|
|
204
|
+
propName: mapPropCandidate.propName,
|
|
205
|
+
expressionPath: mapPropCandidate.expressionPath,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
}
|
|
169
209
|
}
|
|
170
210
|
}
|
|
171
211
|
}
|
|
@@ -224,7 +264,7 @@ export function findElementWithText(
|
|
|
224
264
|
// Recursively visit children
|
|
225
265
|
if ('children' in node && Array.isArray(node.children)) {
|
|
226
266
|
for (const child of node.children) {
|
|
227
|
-
visit(child)
|
|
267
|
+
visit(child, currentExpr)
|
|
228
268
|
}
|
|
229
269
|
}
|
|
230
270
|
}
|
|
@@ -245,10 +285,102 @@ export function findElementWithText(
|
|
|
245
285
|
return null
|
|
246
286
|
}
|
|
247
287
|
|
|
248
|
-
visit(ast)
|
|
288
|
+
visit(ast, null)
|
|
249
289
|
return { bestMatch, propCandidates, importCandidates }
|
|
250
290
|
}
|
|
251
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Collect the joined text of every direct text child of an expression node.
|
|
294
|
+
* Used to feed `resolveMapChain` with the surrounding `.map(...)` source.
|
|
295
|
+
*/
|
|
296
|
+
function collectExpressionText(parentExpression: AstroNode): string[] {
|
|
297
|
+
const exprTexts: string[] = []
|
|
298
|
+
if ('children' in parentExpression && Array.isArray(parentExpression.children)) {
|
|
299
|
+
for (const child of parentExpression.children) {
|
|
300
|
+
if (child.type === 'text' && (child as TextNode).value) {
|
|
301
|
+
exprTexts.push((child as TextNode).value)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return exprTexts
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Resolve a `.map()` loop variable reference (`item.label` or destructured `label`)
|
|
310
|
+
* against local variable definitions, returning the concrete element whose value
|
|
311
|
+
* matches `normalizedSearch`. Returns null when the chain doesn't resolve or no
|
|
312
|
+
* leaf value matches.
|
|
313
|
+
*/
|
|
314
|
+
function resolveMapForLocal(
|
|
315
|
+
exprPath: string,
|
|
316
|
+
parentExpression: AstroNode,
|
|
317
|
+
variableDefinitions: VariableDefinition[],
|
|
318
|
+
normalizedSearch: string,
|
|
319
|
+
): { variableName: string; definitionLine: number } | null {
|
|
320
|
+
const baseMatch = exprPath.match(/^(\w+)(.*)$/)
|
|
321
|
+
if (!baseMatch) return null
|
|
322
|
+
const baseVar = baseMatch[1]!
|
|
323
|
+
const suffix = baseMatch[2] ?? ''
|
|
324
|
+
|
|
325
|
+
const exprTexts = collectExpressionText(parentExpression)
|
|
326
|
+
if (exprTexts.length === 0) return null
|
|
327
|
+
|
|
328
|
+
const resolved = resolveMapChain(exprTexts, baseVar)
|
|
329
|
+
if (!resolved) return null
|
|
330
|
+
|
|
331
|
+
const leafRegex = makeLeafPathRegex({
|
|
332
|
+
arrayPath: resolved.arrayPath,
|
|
333
|
+
leafSuffix: resolved.leafSuffix + suffix,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
for (const def of variableDefinitions) {
|
|
337
|
+
const defPath = buildDefinitionPath(def)
|
|
338
|
+
if (!leafRegex.test(defPath)) continue
|
|
339
|
+
if (normalizeText(def.value) === normalizedSearch) {
|
|
340
|
+
return { variableName: defPath, definitionLine: def.line }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Resolve a `.map()` loop variable reference against prop aliases, returning a
|
|
348
|
+
* cross-file candidate when the source array comes from props. The expression
|
|
349
|
+
* path encodes the wildcard chain (e.g. `items[*].label`) so cross-file lookups
|
|
350
|
+
* can match concrete parent definitions.
|
|
351
|
+
*/
|
|
352
|
+
function resolveMapForProp(
|
|
353
|
+
exprPath: string,
|
|
354
|
+
parentExpression: AstroNode,
|
|
355
|
+
propAliases: Map<string, string>,
|
|
356
|
+
): { propName: string; expressionPath: string } | null {
|
|
357
|
+
const baseMatch = exprPath.match(/^(\w+)(.*)$/)
|
|
358
|
+
if (!baseMatch) return null
|
|
359
|
+
const baseVar = baseMatch[1]!
|
|
360
|
+
const suffix = baseMatch[2] ?? ''
|
|
361
|
+
|
|
362
|
+
const exprTexts = collectExpressionText(parentExpression)
|
|
363
|
+
if (exprTexts.length === 0) return null
|
|
364
|
+
|
|
365
|
+
const resolved = resolveMapChain(exprTexts, baseVar)
|
|
366
|
+
if (!resolved) return null
|
|
367
|
+
|
|
368
|
+
// The head of the resolved arrayPath is the local name of the array; if that
|
|
369
|
+
// local name is a prop alias, we have a cross-file candidate.
|
|
370
|
+
const headMatch = resolved.arrayPath.match(/^(\w+)/)
|
|
371
|
+
if (!headMatch) return null
|
|
372
|
+
const arrayHead = headMatch[1]!
|
|
373
|
+
const actualPropName = propAliases.get(arrayHead)
|
|
374
|
+
if (!actualPropName) return null
|
|
375
|
+
|
|
376
|
+
// Build the expression path the parent should match: e.g. `items[*].label`.
|
|
377
|
+
// Cross-file lookup combines this with the parent's local binding.
|
|
378
|
+
return {
|
|
379
|
+
propName: actualPropName,
|
|
380
|
+
expressionPath: resolved.arrayPath + '[*]' + resolved.leafSuffix + suffix,
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
252
384
|
// ============================================================================
|
|
253
385
|
// Component Prop Finding
|
|
254
386
|
// ============================================================================
|