@nuasite/cms-marker 0.0.80 → 0.0.82

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/src/index.ts CHANGED
@@ -128,8 +128,10 @@ export type { CollectionInfo, MarkdownContent, SourceLocation, VariableReference
128
128
  // Re-export types for consumers
129
129
  export { findCollectionSource, parseMarkdownContent } from './source-finder'
130
130
  export type {
131
+ AriaAttributes,
131
132
  AvailableColors,
132
133
  AvailableTextStyles,
134
+ ButtonAttributes,
133
135
  CanonicalUrl,
134
136
  CmsManifest,
135
137
  CmsMarkerOptions,
@@ -140,16 +142,24 @@ export type {
140
142
  ComponentInstance,
141
143
  ComponentProp,
142
144
  ContentConstraints,
145
+ DataAttributes,
143
146
  FieldDefinition,
144
147
  FieldType,
148
+ FormAttributes,
145
149
  GradientClasses,
150
+ IframeAttributes,
146
151
  ImageMetadata,
152
+ InputAttributes,
147
153
  JsonLdEntry,
154
+ LinkAttributes,
148
155
  ManifestEntry,
149
156
  ManifestMetadata,
157
+ MediaAttributes,
150
158
  OpacityClasses,
151
159
  OpenGraphData,
160
+ PageEntry,
152
161
  PageSeoData,
162
+ SelectAttributes,
153
163
  SeoKeywords,
154
164
  SeoMetaTag,
155
165
  SeoOptions,
@@ -157,6 +167,7 @@ export type {
157
167
  SeoTitle,
158
168
  SourceContext,
159
169
  TailwindColor,
170
+ TextareaAttributes,
160
171
  TextStyleValue,
161
172
  TwitterCardData,
162
173
  } from './types'
@@ -12,6 +12,7 @@ import type {
12
12
  ComponentInstance,
13
13
  ManifestEntry,
14
14
  ManifestMetadata,
15
+ PageEntry,
15
16
  PageSeoData,
16
17
  } from './types'
17
18
  import { generateManifestContentHash, generateSourceFileHashes } from './utils'
@@ -209,7 +210,18 @@ export class ManifestWriter {
209
210
  // Wait for all queued writes to complete
210
211
  await this.writeQueue
211
212
 
212
- // Write global manifest with settings (component definitions, colors, text styles, and collection definitions)
213
+ // Build pages array with pathname and title, sorted by pathname
214
+ const pages: PageEntry[] = Array.from(this.pageManifests.entries())
215
+ .map(([pathname, data]) => {
216
+ const entry: PageEntry = { pathname }
217
+ if (data.seo?.title?.content) {
218
+ entry.title = data.seo.title.content
219
+ }
220
+ return entry
221
+ })
222
+ .sort((a, b) => a.pathname.localeCompare(b.pathname))
223
+
224
+ // Write global manifest with settings (component definitions, colors, text styles, collection definitions, and pages)
213
225
  if (this.outDir) {
214
226
  const globalManifestPath = path.join(this.outDir, this.manifestFile)
215
227
  const globalSettings: {
@@ -217,8 +229,10 @@ export class ManifestWriter {
217
229
  collectionDefinitions?: Record<string, CollectionDefinition>
218
230
  availableColors?: AvailableColors
219
231
  availableTextStyles?: AvailableTextStyles
232
+ pages: PageEntry[]
220
233
  } = {
221
234
  componentDefinitions: this.componentDefinitions,
235
+ pages,
222
236
  }
223
237
  if (Object.keys(this.collectionDefinitions).length > 0) {
224
238
  globalSettings.collectionDefinitions = this.collectionDefinitions
@@ -1,9 +1,9 @@
1
- import fs from 'node:fs/promises'
2
1
  import { type HTMLElement as ParsedHTMLElement, parse } from 'node-html-parser'
2
+ import fs from 'node:fs/promises'
3
3
  import path from 'node:path'
4
4
  import { getProjectRoot } from './config'
5
5
  import { findSourceLocation } from './source-finder/source-lookup'
6
- import type { CanonicalUrl, JsonLdEntry, OpenGraphData, PageSeoData, RobotsDirective, SeoKeywords, SeoMetaTag, SeoTitle, TwitterCardData } from './types'
6
+ import type { CanonicalUrl, JsonLdEntry, OpenGraphData, PageSeoData, SeoKeywords, SeoMetaTag, SeoTitle, TwitterCardData } from './types'
7
7
 
8
8
  /** Type for parsed HTML element nodes from node-html-parser */
9
9
  type HTMLNode = ParsedHTMLElement
@@ -23,7 +23,7 @@ export interface ProcessSeoResult {
23
23
  /** The modified HTML with title CMS ID if markTitle is enabled */
24
24
  html: string
25
25
  /** The CMS ID assigned to the title element */
26
- titleCmsId?: string
26
+ titleId?: string
27
27
  }
28
28
 
29
29
  /**
@@ -50,29 +50,29 @@ export async function processSeoFromHtml(
50
50
 
51
51
  const head = root.querySelector('head')
52
52
  const seo: PageSeoData = {}
53
- let titleCmsId: string | undefined
53
+ let titleId: string | undefined
54
54
 
55
55
  // Extract title
56
56
  const titleResult = await extractTitle(root, html, sourcePath, markTitle, getNextId)
57
57
  if (titleResult) {
58
58
  seo.title = titleResult.title
59
- titleCmsId = titleResult.cmsId
59
+ titleId = titleResult.id
60
60
  }
61
61
 
62
62
  // Extract meta tags from head
63
63
  if (head) {
64
- const metaTags = await extractMetaTags(head, html, sourcePath)
64
+ const metaTags = await extractMetaTags(head, html, sourcePath, getNextId)
65
65
  categorizeMetaTags(metaTags, seo)
66
66
 
67
67
  // Extract canonical URL
68
- const canonical = await extractCanonical(head, html, sourcePath)
68
+ const canonical = await extractCanonical(head, html, sourcePath, getNextId)
69
69
  if (canonical) {
70
70
  seo.canonical = canonical
71
71
  }
72
72
 
73
73
  // Extract JSON-LD
74
74
  if (parseJsonLd) {
75
- const jsonLdEntries = await extractJsonLd(head, html, sourcePath)
75
+ const jsonLdEntries = await extractJsonLd(head, html, sourcePath, getNextId)
76
76
  if (jsonLdEntries.length > 0) {
77
77
  seo.jsonLd = jsonLdEntries
78
78
  }
@@ -82,7 +82,7 @@ export async function processSeoFromHtml(
82
82
  return {
83
83
  seo,
84
84
  html: root.toString(),
85
- titleCmsId,
85
+ titleId,
86
86
  }
87
87
  }
88
88
 
@@ -95,7 +95,7 @@ async function extractTitle(
95
95
  sourcePath?: string,
96
96
  markTitle?: boolean,
97
97
  getNextId?: () => string,
98
- ): Promise<{ title: SeoTitle; cmsId?: string } | undefined> {
98
+ ): Promise<{ title: SeoTitle; id?: string } | undefined> {
99
99
  const titleElement = root.querySelector('title')
100
100
  if (!titleElement) return undefined
101
101
 
@@ -115,19 +115,19 @@ async function extractTitle(
115
115
  }
116
116
  : findElementSourceLocation(titleElement, html, sourcePath)
117
117
 
118
- let cmsId: string | undefined
118
+ let id: string | undefined
119
119
  if (markTitle && getNextId) {
120
- cmsId = getNextId()
121
- titleElement.setAttribute('data-cms-id', cmsId)
120
+ id = getNextId()
121
+ titleElement.setAttribute('data-cms-id', id)
122
122
  }
123
123
 
124
124
  return {
125
125
  title: {
126
126
  content,
127
- cmsId,
127
+ id,
128
128
  ...sourceInfo,
129
129
  },
130
- cmsId,
130
+ id,
131
131
  }
132
132
  }
133
133
 
@@ -138,6 +138,7 @@ async function extractMetaTags(
138
138
  head: HTMLNode,
139
139
  html: string,
140
140
  sourcePath?: string,
141
+ getNextId?: () => string,
141
142
  ): Promise<SeoMetaTag[]> {
142
143
  const metaTags: SeoMetaTag[] = []
143
144
  const metas = head.querySelectorAll('meta')
@@ -150,17 +151,28 @@ async function extractMetaTags(
150
151
  // Skip meta tags without content or without name/property
151
152
  if (!content || (!name && !property)) continue
152
153
 
153
- // Build a tag pattern for context matching (e.g., "meta.*name="description"")
154
- const identifier = name || property
155
- const tagPattern = identifier ? `<meta[^>]*(?:name|property)\\s*=\\s*["']${identifier}["']` : '<meta'
156
-
157
- // Search for the content attribute value in source files
158
- const sourceLocation = await findAttributeValueSource('content', content, tagPattern)
154
+ // Use the same source finding logic as regular text entries
155
+ // This tracks through props, variables, and imports
156
+ const sourceLocation = await findSourceLocation(content, 'meta')
159
157
 
160
158
  // Fall back to rendered HTML location if source not found
161
- const sourceInfo = sourceLocation || findElementSourceLocation(meta, html, sourcePath)
159
+ const sourceInfo = sourceLocation
160
+ ? {
161
+ sourcePath: sourceLocation.file,
162
+ sourceLine: sourceLocation.line,
163
+ sourceSnippet: sourceLocation.snippet || '',
164
+ }
165
+ : findElementSourceLocation(meta, html, sourcePath)
166
+
167
+ // Mark meta tag with CMS ID for editing
168
+ let id: string | undefined
169
+ if (getNextId) {
170
+ id = getNextId()
171
+ meta.setAttribute('data-cms-id', id)
172
+ }
162
173
 
163
174
  metaTags.push({
175
+ id,
164
176
  name: name || undefined,
165
177
  property: property || undefined,
166
178
  content,
@@ -172,7 +184,7 @@ async function extractMetaTags(
172
184
  }
173
185
 
174
186
  /**
175
- * Categorize meta tags into description, keywords, robots, Open Graph and Twitter Card
187
+ * Categorize meta tags into description, keywords, Open Graph and Twitter Card
176
188
  */
177
189
  function categorizeMetaTags(metaTags: SeoMetaTag[], seo: PageSeoData): void {
178
190
  const openGraph: OpenGraphData = {}
@@ -197,16 +209,6 @@ function categorizeMetaTags(metaTags: SeoMetaTag[], seo: PageSeoData): void {
197
209
  continue
198
210
  }
199
211
 
200
- // Robots
201
- if (name === 'robots') {
202
- const directives = content.split(',').map(d => d.trim().toLowerCase()).filter(Boolean)
203
- seo.robots = {
204
- ...meta,
205
- directives,
206
- } as RobotsDirective
207
- continue
208
- }
209
-
210
212
  // Open Graph tags
211
213
  if (property?.startsWith('og:')) {
212
214
  const ogKey = property.replace('og:', '')
@@ -274,6 +276,7 @@ async function extractCanonical(
274
276
  head: HTMLNode,
275
277
  html: string,
276
278
  sourcePath?: string,
279
+ getNextId?: () => string,
277
280
  ): Promise<CanonicalUrl | undefined> {
278
281
  const canonical = head.querySelector('link[rel="canonical"]')
279
282
  if (!canonical) return undefined
@@ -281,13 +284,28 @@ async function extractCanonical(
281
284
  const href = canonical.getAttribute('href')
282
285
  if (!href) return undefined
283
286
 
284
- // Search for the href attribute value in source files
285
- const sourceLocation = await findAttributeValueSource('href', href, '<link[^>]*rel\\s*=\\s*["\'"]canonical["\'"]')
287
+ // Use the same source finding logic as regular text entries
288
+ // This tracks through props, variables, and imports
289
+ const sourceLocation = await findSourceLocation(href, 'link')
286
290
 
287
291
  // Fall back to rendered HTML location if source not found
288
- const sourceInfo = sourceLocation || findElementSourceLocation(canonical, html, sourcePath)
292
+ const sourceInfo = sourceLocation
293
+ ? {
294
+ sourcePath: sourceLocation.file,
295
+ sourceLine: sourceLocation.line,
296
+ sourceSnippet: sourceLocation.snippet || '',
297
+ }
298
+ : findElementSourceLocation(canonical, html, sourcePath)
299
+
300
+ // Mark canonical link with CMS ID for editing
301
+ let id: string | undefined
302
+ if (getNextId) {
303
+ id = getNextId()
304
+ canonical.setAttribute('data-cms-id', id)
305
+ }
289
306
 
290
307
  return {
308
+ id,
291
309
  href,
292
310
  ...sourceInfo,
293
311
  }
@@ -300,6 +318,7 @@ async function extractJsonLd(
300
318
  head: HTMLNode,
301
319
  html: string,
302
320
  sourcePath?: string,
321
+ getNextId?: () => string,
303
322
  ): Promise<JsonLdEntry[]> {
304
323
  const entries: JsonLdEntry[] = []
305
324
 
@@ -321,7 +340,15 @@ async function extractJsonLd(
321
340
  // Fall back to rendered HTML location if source not found
322
341
  const sourceInfo = sourceLocation || findElementSourceLocation(script, html, sourcePath)
323
342
 
343
+ // Mark JSON-LD script with CMS ID for editing
344
+ let id: string | undefined
345
+ if (getNextId) {
346
+ id = getNextId()
347
+ script.setAttribute('data-cms-id', id)
348
+ }
349
+
324
350
  entries.push({
351
+ id,
325
352
  type,
326
353
  data,
327
354
  ...sourceInfo,
@@ -469,145 +496,3 @@ function findElementSourceLocation(
469
496
  sourceSnippet,
470
497
  }
471
498
  }
472
-
473
- /**
474
- * Search for a text value as an attribute value in source files.
475
- * Handles both static values (content="text") and dynamic expressions (content={variable}).
476
- */
477
- async function findAttributeValueSource(
478
- attrName: string,
479
- value: string,
480
- tagPattern?: string,
481
- ): Promise<{ sourcePath: string; sourceLine: number; sourceSnippet: string } | undefined> {
482
- const srcDir = path.join(getProjectRoot(), 'src')
483
- const searchDirs = [
484
- path.join(srcDir, 'pages'),
485
- path.join(srcDir, 'layouts'),
486
- path.join(srcDir, 'components'),
487
- ]
488
-
489
- for (const dir of searchDirs) {
490
- try {
491
- const result = await searchDirForAttributeValue(dir, attrName, value, tagPattern)
492
- if (result) return result
493
- } catch {
494
- // Directory doesn't exist
495
- }
496
- }
497
-
498
- return undefined
499
- }
500
-
501
- /**
502
- * Recursively search a directory for attribute values
503
- */
504
- async function searchDirForAttributeValue(
505
- dir: string,
506
- attrName: string,
507
- value: string,
508
- tagPattern?: string,
509
- ): Promise<{ sourcePath: string; sourceLine: number; sourceSnippet: string } | undefined> {
510
- try {
511
- const entries = await fs.readdir(dir, { withFileTypes: true })
512
-
513
- for (const entry of entries) {
514
- const fullPath = path.join(dir, entry.name)
515
-
516
- if (entry.isDirectory()) {
517
- const result = await searchDirForAttributeValue(fullPath, attrName, value, tagPattern)
518
- if (result) return result
519
- } else if (entry.isFile() && (entry.name.endsWith('.astro') || entry.name.endsWith('.html'))) {
520
- const result = await searchFileForAttributeValue(fullPath, attrName, value, tagPattern)
521
- if (result) return result
522
- }
523
- }
524
- } catch {
525
- // Error reading directory
526
- }
527
-
528
- return undefined
529
- }
530
-
531
- /**
532
- * Search a single file for an attribute value
533
- */
534
- async function searchFileForAttributeValue(
535
- filePath: string,
536
- attrName: string,
537
- value: string,
538
- tagPattern?: string,
539
- ): Promise<{ sourcePath: string; sourceLine: number; sourceSnippet: string } | undefined> {
540
- try {
541
- const content = await fs.readFile(filePath, 'utf-8')
542
- const lines = content.split('\n')
543
-
544
- // Escape special regex characters in the value
545
- const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
546
-
547
- // Pattern to match static attribute: attrName="value" or attrName='value'
548
- const staticPattern = new RegExp(`${attrName}\\s*=\\s*["']${escapedValue}["']`, 'i')
549
-
550
- // Pattern to match the tag context if provided
551
- const tagRegex = tagPattern ? new RegExp(tagPattern, 'i') : null
552
-
553
- for (let i = 0; i < lines.length; i++) {
554
- const line = lines[i] || ''
555
-
556
- // Check if this line matches the attribute pattern
557
- if (staticPattern.test(line)) {
558
- // If tag pattern provided, verify we're in the right context
559
- if (tagRegex && !tagRegex.test(line)) {
560
- // Check surrounding lines for tag context
561
- const contextLines = lines.slice(Math.max(0, i - 3), i + 1).join(' ')
562
- if (!tagRegex.test(contextLines)) {
563
- continue
564
- }
565
- }
566
-
567
- // Extract the full element snippet
568
- const snippet = extractElementSnippetFromLines(lines, i, tagPattern)
569
-
570
- return {
571
- sourcePath: path.relative(getProjectRoot(), filePath),
572
- sourceLine: i + 1,
573
- sourceSnippet: snippet,
574
- }
575
- }
576
- }
577
- } catch {
578
- // Error reading file
579
- }
580
-
581
- return undefined
582
- }
583
-
584
- /**
585
- * Extract a multi-line element snippet starting from a given line
586
- */
587
- function extractElementSnippetFromLines(lines: string[], startLine: number, tagPattern?: string): string {
588
- const snippetLines: string[] = []
589
-
590
- // Look backwards to find the tag opening if we're on an attribute line
591
- let actualStart = startLine
592
- for (let i = startLine; i >= Math.max(0, startLine - 5); i--) {
593
- const line = lines[i] || ''
594
- if (line.includes('<meta') || line.includes('<link') || line.includes('<title') || line.includes('<script')) {
595
- actualStart = i
596
- break
597
- }
598
- }
599
-
600
- // Collect lines until we find the closing
601
- for (let i = actualStart; i < Math.min(actualStart + 10, lines.length); i++) {
602
- const line = lines[i]
603
- if (!line) continue
604
- snippetLines.push(line)
605
-
606
- // Check for self-closing or closing tag
607
- if (line.includes('/>') || line.includes('</') || (line.includes('>') && !line.includes('<'))) {
608
- break
609
- }
610
- }
611
-
612
- return snippetLines.join('\n')
613
- }