@nuasite/cms-marker 0.0.71 → 0.0.73

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 (48) hide show
  1. package/dist/types/build-processor.d.ts.map +1 -1
  2. package/dist/types/dev-middleware.d.ts.map +1 -1
  3. package/dist/types/source-finder/ast-extractors.d.ts +35 -0
  4. package/dist/types/source-finder/ast-extractors.d.ts.map +1 -0
  5. package/dist/types/source-finder/ast-parser.d.ts +16 -0
  6. package/dist/types/source-finder/ast-parser.d.ts.map +1 -0
  7. package/dist/types/source-finder/cache.d.ts +18 -0
  8. package/dist/types/source-finder/cache.d.ts.map +1 -0
  9. package/dist/types/source-finder/collection-finder.d.ts +24 -0
  10. package/dist/types/source-finder/collection-finder.d.ts.map +1 -0
  11. package/dist/types/source-finder/cross-file-tracker.d.ts +29 -0
  12. package/dist/types/source-finder/cross-file-tracker.d.ts.map +1 -0
  13. package/dist/types/source-finder/element-finder.d.ts +42 -0
  14. package/dist/types/source-finder/element-finder.d.ts.map +1 -0
  15. package/dist/types/source-finder/image-finder.d.ts +16 -0
  16. package/dist/types/source-finder/image-finder.d.ts.map +1 -0
  17. package/dist/types/source-finder/index.d.ts +8 -0
  18. package/dist/types/source-finder/index.d.ts.map +1 -0
  19. package/dist/types/source-finder/search-index.d.ts +27 -0
  20. package/dist/types/source-finder/search-index.d.ts.map +1 -0
  21. package/dist/types/source-finder/snippet-utils.d.ts +49 -0
  22. package/dist/types/source-finder/snippet-utils.d.ts.map +1 -0
  23. package/dist/types/source-finder/source-lookup.d.ts +16 -0
  24. package/dist/types/source-finder/source-lookup.d.ts.map +1 -0
  25. package/dist/types/source-finder/types.d.ts +163 -0
  26. package/dist/types/source-finder/types.d.ts.map +1 -0
  27. package/dist/types/source-finder/variable-extraction.d.ts +37 -0
  28. package/dist/types/source-finder/variable-extraction.d.ts.map +1 -0
  29. package/dist/types/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +1 -1
  31. package/src/build-processor.ts +33 -1
  32. package/src/dev-middleware.ts +33 -1
  33. package/src/source-finder/ast-extractors.ts +175 -0
  34. package/src/source-finder/ast-parser.ts +127 -0
  35. package/src/source-finder/cache.ts +75 -0
  36. package/src/source-finder/collection-finder.ts +321 -0
  37. package/src/source-finder/cross-file-tracker.ts +337 -0
  38. package/src/source-finder/element-finder.ts +383 -0
  39. package/src/source-finder/image-finder.ts +189 -0
  40. package/src/source-finder/index.ts +26 -0
  41. package/src/source-finder/search-index.ts +418 -0
  42. package/src/source-finder/snippet-utils.ts +268 -0
  43. package/src/source-finder/source-lookup.ts +197 -0
  44. package/src/source-finder/types.ts +206 -0
  45. package/src/source-finder/variable-extraction.ts +355 -0
  46. package/dist/types/source-finder.d.ts +0 -117
  47. package/dist/types/source-finder.d.ts.map +0 -1
  48. package/src/source-finder.ts +0 -1784
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "directory": "packages/cms-marker"
15
15
  },
16
16
  "license": "Apache-2.0",
17
- "version": "0.0.71",
17
+ "version": "0.0.73",
18
18
  "module": "src/index.ts",
19
19
  "types": "src/index.ts",
20
20
  "type": "module",
@@ -1,4 +1,5 @@
1
1
  import type { AstroIntegrationLogger } from 'astro'
2
+ import { parse } from 'node-html-parser'
2
3
  import fs from 'node:fs/promises'
3
4
  import path from 'node:path'
4
5
  import { fileURLToPath } from 'node:url'
@@ -165,11 +166,42 @@ async function processFile(
165
166
 
166
167
  await Promise.all(entryLookups)
167
168
 
169
+ // Filter out entries without sourcePath - these can't be edited
170
+ const idsToRemove: string[] = []
171
+ for (const [id, entry] of Object.entries(result.entries)) {
172
+ // Keep collection wrapper entries even without sourcePath (they use contentPath)
173
+ if (entry.sourceType === 'collection') continue
174
+ // Remove entries that don't have a resolved sourcePath
175
+ if (!entry.sourcePath) {
176
+ idsToRemove.push(id)
177
+ delete result.entries[id]
178
+ }
179
+ }
180
+
181
+ // Remove CMS ID attributes from HTML for entries that were filtered out
182
+ let finalHtml = result.html
183
+ if (idsToRemove.length > 0) {
184
+ const root = parse(result.html, {
185
+ lowerCaseTagName: false,
186
+ comment: true,
187
+ })
188
+ for (const id of idsToRemove) {
189
+ const element = root.querySelector(`[${config.attributeName}="${id}"]`)
190
+ if (element) {
191
+ element.removeAttribute(config.attributeName)
192
+ // Also remove related CMS attributes
193
+ element.removeAttribute('data-cms-img')
194
+ element.removeAttribute('data-cms-markdown')
195
+ }
196
+ }
197
+ finalHtml = root.toString()
198
+ }
199
+
168
200
  // Add to manifest writer (handles per-page manifest writes)
169
201
  manifestWriter.addPage(pagePath, result.entries, result.components, collectionEntry)
170
202
 
171
203
  // Write transformed HTML back
172
- await fs.writeFile(filePath, result.html, 'utf-8')
204
+ await fs.writeFile(filePath, finalHtml, 'utf-8')
173
205
 
174
206
  return Object.keys(result.entries).length
175
207
  }
@@ -1,3 +1,4 @@
1
+ import { parse } from 'node-html-parser'
1
2
  import type { IncomingMessage, ServerResponse } from 'node:http'
2
3
  import { processHtml } from './html-processor'
3
4
  import type { ManifestWriter } from './manifest-writer'
@@ -237,8 +238,39 @@ async function processHtmlForDev(
237
238
  }
238
239
  }
239
240
 
241
+ // Filter out entries without sourcePath - these can't be edited
242
+ const idsToRemove: string[] = []
243
+ for (const [id, entry] of Object.entries(result.entries)) {
244
+ // Keep collection wrapper entries even without sourcePath (they use contentPath)
245
+ if (entry.sourceType === 'collection') continue
246
+ // Remove entries that don't have a resolved sourcePath
247
+ if (!entry.sourcePath) {
248
+ idsToRemove.push(id)
249
+ delete result.entries[id]
250
+ }
251
+ }
252
+
253
+ // Remove CMS ID attributes from HTML for entries that were filtered out
254
+ let finalHtml = result.html
255
+ if (idsToRemove.length > 0) {
256
+ const root = parse(result.html, {
257
+ lowerCaseTagName: false,
258
+ comment: true,
259
+ })
260
+ for (const id of idsToRemove) {
261
+ const element = root.querySelector(`[${config.attributeName}="${id}"]`)
262
+ if (element) {
263
+ element.removeAttribute(config.attributeName)
264
+ // Also remove related CMS attributes
265
+ element.removeAttribute('data-cms-img')
266
+ element.removeAttribute('data-cms-markdown')
267
+ }
268
+ }
269
+ finalHtml = root.toString()
270
+ }
271
+
240
272
  return {
241
- html: result.html,
273
+ html: finalHtml,
242
274
  entries: result.entries,
243
275
  components: result.components,
244
276
  collection: collectionEntry,
@@ -0,0 +1,175 @@
1
+ import type { BabelNode, LineTransformer, VariableDefinition } from './types'
2
+
3
+ // ============================================================================
4
+ // String Value Extraction
5
+ // ============================================================================
6
+
7
+ /**
8
+ * Extract string value from a Babel node (StringLiteral or simple TemplateLiteral)
9
+ */
10
+ export function getStringValue(node: BabelNode): string | null {
11
+ if (node.type === 'StringLiteral') {
12
+ return node.value as string
13
+ }
14
+ if (node.type === 'TemplateLiteral') {
15
+ const quasis = node.quasis as Array<{ value: { cooked: string | null } }> | undefined
16
+ const expressions = node.expressions as unknown[] | undefined
17
+ if (quasis?.length === 1 && expressions?.length === 0) {
18
+ return quasis[0]?.value.cooked ?? null
19
+ }
20
+ }
21
+ return null
22
+ }
23
+
24
+ // ============================================================================
25
+ // Object and Array Extraction
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Recursively extract properties from an object expression
30
+ * @param objNode - The ObjectExpression node
31
+ * @param parentPath - The full path to this object (e.g., 'config' or 'config.nav')
32
+ * @param definitions - Array to collect definitions into
33
+ * @param lineTransformer - Transforms Babel line numbers to file line numbers
34
+ */
35
+ export function extractObjectProperties(
36
+ objNode: BabelNode,
37
+ parentPath: string,
38
+ definitions: VariableDefinition[],
39
+ lineTransformer: LineTransformer,
40
+ ): void {
41
+ const properties = objNode.properties as BabelNode[] | undefined
42
+ for (const prop of properties ?? []) {
43
+ if (prop.type !== 'ObjectProperty') continue
44
+ const key = prop.key as BabelNode | undefined
45
+ const value = prop.value as BabelNode | undefined
46
+ if (!key || key.type !== 'Identifier' || !value) continue
47
+
48
+ const propName = key.name as string
49
+ const fullPath = `${parentPath}.${propName}`
50
+ const propLoc = prop.loc as { start: { line: number } } | undefined
51
+ const propLine = lineTransformer(propLoc?.start.line ?? 1)
52
+
53
+ const stringValue = getStringValue(value)
54
+ if (stringValue !== null) {
55
+ definitions.push({
56
+ name: propName,
57
+ value: stringValue,
58
+ line: propLine,
59
+ parentName: parentPath,
60
+ })
61
+ }
62
+
63
+ // Recurse for nested objects
64
+ if (value.type === 'ObjectExpression') {
65
+ extractObjectProperties(value, fullPath, definitions, lineTransformer)
66
+ }
67
+
68
+ // Handle arrays within objects
69
+ if (value.type === 'ArrayExpression') {
70
+ extractArrayElements(value, fullPath, definitions, lineTransformer, propLine)
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Extract elements from an array expression
77
+ * @param arrNode - The ArrayExpression node
78
+ * @param parentPath - The full path to this array (e.g., 'items' or 'config.items')
79
+ * @param definitions - Array to collect definitions into
80
+ * @param lineTransformer - Transforms Babel line numbers to file line numbers
81
+ * @param defaultLine - Fallback line if element has no location
82
+ */
83
+ export function extractArrayElements(
84
+ arrNode: BabelNode,
85
+ parentPath: string,
86
+ definitions: VariableDefinition[],
87
+ lineTransformer: LineTransformer,
88
+ defaultLine: number,
89
+ ): void {
90
+ const elements = arrNode.elements as BabelNode[] | undefined
91
+ for (let i = 0; i < (elements?.length ?? 0); i++) {
92
+ const elem = elements![i]
93
+ if (!elem) continue
94
+
95
+ const elemLoc = elem.loc as { start: { line: number } } | undefined
96
+ const elemLine = elemLoc ? lineTransformer(elemLoc.start.line) : defaultLine
97
+ const indexPath = `${parentPath}[${i}]`
98
+
99
+ // Handle string values in array
100
+ const elemValue = getStringValue(elem)
101
+ if (elemValue !== null) {
102
+ definitions.push({
103
+ name: String(i),
104
+ value: elemValue,
105
+ line: elemLine,
106
+ parentName: parentPath,
107
+ })
108
+ }
109
+
110
+ // Handle array of objects: [{ text: 'Home' }]
111
+ if (elem.type === 'ObjectExpression') {
112
+ const objProperties = elem.properties as BabelNode[] | undefined
113
+ for (const prop of objProperties ?? []) {
114
+ if (prop.type !== 'ObjectProperty') continue
115
+ const key = prop.key as BabelNode | undefined
116
+ const value = prop.value as BabelNode | undefined
117
+ if (!key || key.type !== 'Identifier' || !value) continue
118
+
119
+ const propName = key.name as string
120
+ const propLoc = prop.loc as { start: { line: number } } | undefined
121
+ const propLine = propLoc ? lineTransformer(propLoc.start.line) : elemLine
122
+
123
+ const stringValue = getStringValue(value)
124
+ if (stringValue !== null) {
125
+ definitions.push({
126
+ name: propName,
127
+ value: stringValue,
128
+ line: propLine,
129
+ parentName: indexPath,
130
+ })
131
+ }
132
+
133
+ // Recurse for nested objects within array elements
134
+ if (value.type === 'ObjectExpression') {
135
+ extractObjectProperties(value, `${indexPath}.${propName}`, definitions, lineTransformer)
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ // ============================================================================
143
+ // Path Building Utilities
144
+ // ============================================================================
145
+
146
+ /**
147
+ * Build the full path for a variable definition.
148
+ * For array indices (numeric names), uses bracket notation: items[0]
149
+ * For object properties, uses dot notation: config.nav.title
150
+ */
151
+ export function buildDefinitionPath(def: VariableDefinition): string {
152
+ if (!def.parentName) {
153
+ return def.name
154
+ }
155
+ // Check if the name is a numeric index (for arrays)
156
+ if (/^\d+$/.test(def.name)) {
157
+ return `${def.parentName}[${def.name}]`
158
+ }
159
+ return `${def.parentName}.${def.name}`
160
+ }
161
+
162
+ /**
163
+ * Parse an expression path and extract the full path for variable lookup.
164
+ * Handles patterns like: varName, obj.prop, items[0], config.nav.title, links[0].text
165
+ * @returns The full expression path or null if not a simple variable reference
166
+ */
167
+ export function parseExpressionPath(exprText: string): string | null {
168
+ // Match patterns like: varName, obj.prop, items[0], config.nav.title, links[0].text
169
+ // Pattern breakdown: word characters, dots, and bracket notation with numbers
170
+ const match = exprText.match(/^\s*([\w]+(?:\.[\w]+|\[\d+\])*(?:\.[\w]+)?)\s*$/)
171
+ if (match) {
172
+ return match[1]!
173
+ }
174
+ return null
175
+ }
@@ -0,0 +1,127 @@
1
+ import { parse as parseAstro } from '@astrojs/compiler'
2
+ import type { Node as AstroNode } from '@astrojs/compiler/types'
3
+ import { parse as parseBabel } from '@babel/parser'
4
+ import fs from 'node:fs/promises'
5
+
6
+ import { getErrorCollector } from '../error-collector'
7
+ import { getParsedFileCache } from './cache'
8
+ import type { BabelFile, CachedParsedFile, ParsedAstroFile } from './types'
9
+ import { extractImports, extractPropAliases, extractVariableDefinitions } from './variable-extraction'
10
+
11
+ // ============================================================================
12
+ // Astro File Parsing
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Parse an Astro file and return both template AST and frontmatter content
17
+ */
18
+ export async function parseAstroFile(content: string): Promise<ParsedAstroFile> {
19
+ const result = await parseAstro(content, { position: true })
20
+
21
+ // Find frontmatter node
22
+ let frontmatterContent: string | null = null
23
+ let frontmatterStartLine = 0
24
+
25
+ for (const child of result.ast.children) {
26
+ if (child.type === 'frontmatter') {
27
+ frontmatterContent = child.value
28
+ frontmatterStartLine = child.position?.start.line ?? 1
29
+ break
30
+ }
31
+ }
32
+
33
+ return {
34
+ ast: result.ast,
35
+ frontmatterContent,
36
+ frontmatterStartLine,
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Parse frontmatter JavaScript/TypeScript with Babel
42
+ * @param content - The frontmatter content to parse
43
+ * @param filePath - Optional file path for error reporting
44
+ */
45
+ export function parseFrontmatter(content: string, filePath?: string): BabelFile | null {
46
+ try {
47
+ return parseBabel(content, {
48
+ sourceType: 'module',
49
+ plugins: ['typescript'],
50
+ errorRecovery: true,
51
+ }) as unknown as BabelFile
52
+ } catch (error) {
53
+ // Record parse errors for aggregated reporting
54
+ if (filePath) {
55
+ getErrorCollector().addWarning(
56
+ `Frontmatter parse: ${filePath}`,
57
+ error instanceof Error ? error.message : String(error),
58
+ )
59
+ }
60
+ return null
61
+ }
62
+ }
63
+
64
+ // ============================================================================
65
+ // Cached File Access
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Get a cached parsed file, parsing it if not cached
70
+ */
71
+ export async function getCachedParsedFile(filePath: string): Promise<CachedParsedFile | null> {
72
+ const cache = getParsedFileCache()
73
+ const cached = cache.get(filePath)
74
+ if (cached) return cached
75
+
76
+ try {
77
+ const content = await fs.readFile(filePath, 'utf-8')
78
+ const lines = content.split('\n')
79
+
80
+ // Only parse .astro files with AST
81
+ if (!filePath.endsWith('.astro')) {
82
+ // For tsx/jsx, just cache content/lines for regex search
83
+ const entry: CachedParsedFile = {
84
+ content,
85
+ lines,
86
+ ast: { type: 'root', children: [] } as unknown as AstroNode,
87
+ frontmatterContent: null,
88
+ frontmatterStartLine: 0,
89
+ variableDefinitions: [],
90
+ propAliases: new Map(),
91
+ imports: [],
92
+ }
93
+ cache.set(filePath, entry)
94
+ return entry
95
+ }
96
+
97
+ const { ast, frontmatterContent, frontmatterStartLine } = await parseAstroFile(content)
98
+
99
+ let variableDefinitions: CachedParsedFile['variableDefinitions'] = []
100
+ let propAliases: Map<string, string> = new Map()
101
+ let imports: CachedParsedFile['imports'] = []
102
+ if (frontmatterContent) {
103
+ const frontmatterAst = parseFrontmatter(frontmatterContent, filePath)
104
+ if (frontmatterAst) {
105
+ variableDefinitions = extractVariableDefinitions(frontmatterAst, frontmatterStartLine)
106
+ propAliases = extractPropAliases(frontmatterAst)
107
+ imports = extractImports(frontmatterAst)
108
+ }
109
+ }
110
+
111
+ const entry: CachedParsedFile = {
112
+ content,
113
+ lines,
114
+ ast,
115
+ frontmatterContent,
116
+ frontmatterStartLine,
117
+ variableDefinitions,
118
+ propAliases,
119
+ imports,
120
+ }
121
+
122
+ cache.set(filePath, entry)
123
+ return entry
124
+ } catch {
125
+ return null
126
+ }
127
+ }
@@ -0,0 +1,75 @@
1
+ import type { CachedParsedFile, ImageIndexEntry, SearchIndexEntry } from './types'
2
+
3
+ // ============================================================================
4
+ // File Parsing Cache - Avoid re-parsing the same files
5
+ // ============================================================================
6
+
7
+ /** Cache for parsed Astro files - cleared between builds */
8
+ const parsedFileCache = new Map<string, CachedParsedFile>()
9
+
10
+ /** Cache for directory listings - cleared between builds */
11
+ const directoryCache = new Map<string, string[]>()
12
+
13
+ /** Cache for markdown file contents - cleared between builds */
14
+ const markdownFileCache = new Map<string, { content: string; lines: string[] }>()
15
+
16
+ /** Search indexes built once per build */
17
+ let textSearchIndex: SearchIndexEntry[] = []
18
+ let imageSearchIndex: ImageIndexEntry[] = []
19
+ let searchIndexInitialized = false
20
+
21
+ // ============================================================================
22
+ // Cache Access Functions
23
+ // ============================================================================
24
+
25
+ export function getParsedFileCache(): Map<string, CachedParsedFile> {
26
+ return parsedFileCache
27
+ }
28
+
29
+ export function getDirectoryCache(): Map<string, string[]> {
30
+ return directoryCache
31
+ }
32
+
33
+ export function getMarkdownFileCache(): Map<string, { content: string; lines: string[] }> {
34
+ return markdownFileCache
35
+ }
36
+
37
+ export function getTextSearchIndex(): SearchIndexEntry[] {
38
+ return textSearchIndex
39
+ }
40
+
41
+ export function getImageSearchIndex(): ImageIndexEntry[] {
42
+ return imageSearchIndex
43
+ }
44
+
45
+ export function isSearchIndexInitialized(): boolean {
46
+ return searchIndexInitialized
47
+ }
48
+
49
+ export function setSearchIndexInitialized(value: boolean): void {
50
+ searchIndexInitialized = value
51
+ }
52
+
53
+ export function addToTextSearchIndex(entry: SearchIndexEntry): void {
54
+ textSearchIndex.push(entry)
55
+ }
56
+
57
+ export function addToImageSearchIndex(entry: ImageIndexEntry): void {
58
+ imageSearchIndex.push(entry)
59
+ }
60
+
61
+ // ============================================================================
62
+ // Cache Clear Function
63
+ // ============================================================================
64
+
65
+ /**
66
+ * Clear all caches - call at start of each build
67
+ */
68
+ export function clearSourceFinderCache(): void {
69
+ parsedFileCache.clear()
70
+ directoryCache.clear()
71
+ markdownFileCache.clear()
72
+ textSearchIndex = []
73
+ imageSearchIndex = []
74
+ searchIndexInitialized = false
75
+ }