design-clone 2.1.0 → 3.0.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.
Files changed (177) hide show
  1. package/README.md +13 -34
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -171
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +9 -86
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +7 -14
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +2 -22
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +533 -286
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +14 -17
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/css/filter-css-dead-code.js +120 -0
  51. package/src/core/css/filter-css-html-analyzer.js +110 -0
  52. package/src/core/css/filter-css-selector-matcher.js +172 -0
  53. package/src/core/css/filter-css.js +206 -0
  54. package/src/core/css/merge-css-atrule-processor.js +158 -0
  55. package/src/core/css/merge-css-file-io.js +68 -0
  56. package/src/core/css/merge-css.js +148 -0
  57. package/src/core/detection/framework-detector-routing.js +68 -0
  58. package/src/core/detection/framework-detector-signals.js +65 -0
  59. package/src/core/detection/framework-detector.js +198 -0
  60. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  61. package/src/core/dimension/dimension-extractor.js +317 -0
  62. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  63. package/src/core/dimension/dimension-output.js +173 -0
  64. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  65. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  66. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  67. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  68. package/src/core/discovery/app-state-snapshot.js +131 -0
  69. package/src/core/discovery/discover-pages-routes.js +84 -0
  70. package/src/core/discovery/discover-pages-utils.js +177 -0
  71. package/src/core/discovery/discover-pages.js +191 -0
  72. package/src/core/html/html-extractor-inline-styler.js +70 -0
  73. package/src/core/html/html-extractor.js +147 -0
  74. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  75. package/src/core/html/semantic-enhancer-page.js +148 -0
  76. package/src/core/html/semantic-enhancer.js +135 -0
  77. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  78. package/src/core/links/rewrite-links.js +173 -0
  79. package/src/core/media/asset-validator.js +118 -0
  80. package/src/core/media/extract-assets-downloader.js +187 -0
  81. package/src/core/media/extract-assets-page-scraper.js +115 -0
  82. package/src/core/media/extract-assets.js +159 -0
  83. package/src/core/media/video-capture-convert.js +200 -0
  84. package/src/core/media/video-capture.js +201 -0
  85. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
  86. package/src/core/section/section-cropper-helpers.js +43 -0
  87. package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
  88. package/src/core/section/section-detector-strategies.js +139 -0
  89. package/src/core/section/section-detector-utils.js +100 -0
  90. package/src/core/section/section-detector.js +88 -0
  91. package/src/core/tests/test-section-cropper.js +2 -2
  92. package/src/core/tests/test-section-detector.js +2 -2
  93. package/src/post-process/enhance-assets.js +29 -4
  94. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  95. package/src/post-process/fetch-images.js +60 -263
  96. package/src/post-process/inject-gosnap.js +88 -0
  97. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  98. package/src/post-process/inject-icons.js +47 -200
  99. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  100. package/src/route-discoverers/base-discoverer.js +29 -118
  101. package/src/route-discoverers/index.js +1 -1
  102. package/src/shared/config.js +38 -0
  103. package/src/shared/error-codes.js +31 -0
  104. package/src/shared/viewports.js +46 -0
  105. package/src/utils/browser.js +0 -7
  106. package/src/utils/helpers.js +4 -0
  107. package/src/utils/log.js +12 -0
  108. package/src/utils/playwright-loader.js +76 -0
  109. package/src/utils/playwright.js +3 -69
  110. package/src/utils/progress.js +32 -0
  111. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  112. package/src/verification/generate-audit-report-sections.js +158 -0
  113. package/src/verification/generate-audit-report.js +5 -281
  114. package/src/verification/quality-scorer.js +92 -0
  115. package/src/verification/verify-footer-checks.js +103 -0
  116. package/src/verification/verify-footer-helpers.js +178 -0
  117. package/src/verification/verify-footer.js +23 -381
  118. package/src/verification/verify-header-checks.js +104 -0
  119. package/src/verification/verify-header-helpers.js +156 -0
  120. package/src/verification/verify-header.js +23 -365
  121. package/src/verification/verify-layout-report.js +101 -0
  122. package/src/verification/verify-layout.js +13 -259
  123. package/src/verification/verify-menu-checks.js +104 -0
  124. package/src/verification/verify-menu-helpers.js +112 -0
  125. package/src/verification/verify-menu.js +17 -285
  126. package/src/verification/verify-slider-checks.js +115 -0
  127. package/src/verification/verify-slider-constants.js +65 -0
  128. package/src/verification/verify-slider-helpers.js +164 -0
  129. package/src/verification/verify-slider.js +23 -414
  130. package/.env.example +0 -14
  131. package/docs/basic-clone.md +0 -63
  132. package/docs/cli-reference.md +0 -316
  133. package/docs/design-clone-architecture.md +0 -492
  134. package/docs/pixel-perfect.md +0 -117
  135. package/docs/project-roadmap.md +0 -382
  136. package/docs/troubleshooting.md +0 -170
  137. package/requirements.txt +0 -5
  138. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  139. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  140. package/src/ai/analyze-structure.py +0 -375
  141. package/src/ai/extract-design-tokens.py +0 -782
  142. package/src/ai/prompts/__init__.py +0 -2
  143. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  144. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  145. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  146. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  147. package/src/ai/prompts/design_tokens.py +0 -316
  148. package/src/ai/prompts/structure_analysis.py +0 -592
  149. package/src/ai/prompts/ux_audit.py +0 -198
  150. package/src/ai/ux-audit.js +0 -596
  151. package/src/core/animation-extractor.js +0 -526
  152. package/src/core/app-state-snapshot.js +0 -511
  153. package/src/core/content-counter.js +0 -342
  154. package/src/core/design-tokens.js +0 -103
  155. package/src/core/dimension-extractor.js +0 -438
  156. package/src/core/dimension-output.js +0 -305
  157. package/src/core/discover-pages.js +0 -542
  158. package/src/core/dom-tree-analyzer.js +0 -298
  159. package/src/core/extract-assets.js +0 -468
  160. package/src/core/filter-css.js +0 -499
  161. package/src/core/framework-detector.js +0 -538
  162. package/src/core/html-extractor.js +0 -212
  163. package/src/core/merge-css.js +0 -407
  164. package/src/core/multi-page-screenshot.js +0 -380
  165. package/src/core/rewrite-links.js +0 -226
  166. package/src/core/screenshot.js +0 -701
  167. package/src/core/section-detector.js +0 -386
  168. package/src/core/semantic-enhancer.js +0 -492
  169. package/src/core/state-capture.js +0 -598
  170. package/src/core/video-capture.js +0 -546
  171. package/src/utils/__init__.py +0 -16
  172. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  173. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  174. package/src/utils/env.py +0 -134
  175. /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
  176. /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
  177. /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * SVG Icon Replacer Utilities
3
+ *
4
+ * Handles finding SVG placeholder elements in HTML and preserving
5
+ * original attributes (class, width, height, aria-*) when injecting
6
+ * replacement icons.
7
+ */
8
+
9
+ /**
10
+ * Extract surrounding context text from HTML at a given position.
11
+ * @param {string} html
12
+ * @param {number} position - Character index of the SVG element
13
+ * @param {number} range - Characters to capture before and after
14
+ * @returns {string}
15
+ */
16
+ export function extractContext(html, position, range = 200) {
17
+ const start = Math.max(0, position - range);
18
+ const end = Math.min(html.length, position + range);
19
+ return html.slice(start, end);
20
+ }
21
+
22
+ /**
23
+ * Find SVG elements that are candidates for icon replacement.
24
+ * Skips logos (text elements, >6 path nodes, logo/brand class).
25
+ * @param {string} html
26
+ * @param {Function} detectPurpose - detectIconPurpose(svgTag, context) → string
27
+ * @returns {Array<{ original, position, context, purpose }>}
28
+ */
29
+ export function findSvgElements(html, detectPurpose) {
30
+ const elements = [];
31
+ const svgRegex = /<svg[^>]*viewBox=["'][^"']*["'][^>]*>[\s\S]*?<\/svg>/gi;
32
+ let match;
33
+
34
+ while ((match = svgRegex.exec(html)) !== null) {
35
+ const svgTag = match[0];
36
+ const context = extractContext(html, match.index);
37
+
38
+ const pathCount = (svgTag.match(/<(path|circle|rect|line|polyline|polygon)/gi) || []).length;
39
+
40
+ // Skip complex SVGs (likely logos or illustrations)
41
+ if (svgTag.includes('<text') || pathCount > 6) continue;
42
+ if (/class=["'][^"']*(logo|brand)[^"']*["']/i.test(svgTag)) continue;
43
+
44
+ elements.push({
45
+ original: svgTag,
46
+ position: match.index,
47
+ context,
48
+ purpose: detectPurpose(svgTag, context)
49
+ });
50
+ }
51
+
52
+ return elements;
53
+ }
54
+
55
+ /**
56
+ * Preserve original SVG attributes (class, width, height, aria-*)
57
+ * when replacing with a new icon SVG string.
58
+ * @param {string} originalSvg - The original SVG markup
59
+ * @param {string} newSvg - The replacement icon SVG markup
60
+ * @returns {string} New SVG with original attributes preserved
61
+ */
62
+ export function preserveAttributes(originalSvg, newSvg) {
63
+ const classMatch = originalSvg.match(/class=["']([^"']*)["']/i);
64
+ const widthMatch = originalSvg.match(/width=["']([^"']*)["']/i);
65
+ const heightMatch = originalSvg.match(/height=["']([^"']*)["']/i);
66
+ const ariaMatch = originalSvg.match(/aria-[^=]+=["'][^"']*["']/gi);
67
+
68
+ let result = newSvg;
69
+
70
+ if (classMatch) result = result.replace('<svg', `<svg class="${classMatch[1]}"`);
71
+ if (widthMatch) result = result.replace('<svg', `<svg width="${widthMatch[1]}"`);
72
+ if (heightMatch) result = result.replace('<svg', `<svg height="${heightMatch[1]}"`);
73
+ if (ariaMatch) result = result.replace('<svg', `<svg ${ariaMatch.join(' ')}`);
74
+
75
+ return result;
76
+ }
@@ -12,85 +12,52 @@
12
12
  */
13
13
 
14
14
  import fs from 'fs/promises';
15
- import path from 'path';
16
- import { icons, iconMapping, getIcon, getIconByKeyword } from './icons/japanese-icons.js';
15
+ import { icons, iconMapping, getIcon } from './icons/japanese-icons.js';
16
+ import { parseArgs as parseRawArgs } from '../utils/helpers.js';
17
+ import { findSvgElements, preserveAttributes } from './inject-icons-svg-replacer.js';
17
18
 
18
19
  /**
19
- * Parse command line arguments
20
+ * Parse command line arguments.
21
+ * @returns {{ html: string|null, verbose: boolean }}
20
22
  */
21
23
  function parseArgs() {
22
- const args = process.argv.slice(2);
23
- const options = {
24
- html: null,
25
- verbose: false
24
+ const raw = parseRawArgs(process.argv.slice(2));
25
+ return {
26
+ html: raw.html || null,
27
+ verbose: raw.verbose === true || raw.verbose === 'true'
26
28
  };
27
-
28
- for (let i = 0; i < args.length; i++) {
29
- switch (args[i]) {
30
- case '--html':
31
- options.html = args[++i];
32
- break;
33
- case '--verbose':
34
- options.verbose = true;
35
- break;
36
- }
37
- }
38
-
39
- return options;
40
- }
41
-
42
- /**
43
- * Extract context from element's surrounding HTML
44
- */
45
- function extractContext(html, position, range = 200) {
46
- const start = Math.max(0, position - range);
47
- const end = Math.min(html.length, position + range);
48
- return html.slice(start, end);
49
29
  }
50
30
 
51
31
  /**
52
- * Detect icon purpose from class names, aria-label, or surrounding text
32
+ * Detect icon purpose from class names, aria-label, or surrounding text.
33
+ * @param {string} svgTag - The SVG opening tag markup
34
+ * @param {string} context - Surrounding HTML context string
35
+ * @returns {string} Matched keyword or 'decorative'
53
36
  */
54
- function detectIconPurpose(svgTag, context) {
55
- // Check class names
37
+ export function detectIconPurpose(svgTag, context) {
56
38
  const classMatch = svgTag.match(/class=["']([^"']*)["']/i);
57
39
  if (classMatch) {
58
40
  const classes = classMatch[1].toLowerCase();
59
-
60
- // Check for icon type in class
61
41
  for (const keyword of Object.keys(iconMapping)) {
62
- if (classes.includes(keyword)) {
63
- return keyword;
64
- }
42
+ if (classes.includes(keyword)) return keyword;
65
43
  }
66
-
67
- // Check for category hints
68
44
  if (classes.includes('icon')) {
69
- // Try to extract purpose from class like "icon-mail" or "mail-icon"
70
45
  const parts = classes.split(/[-_\s]+/);
71
46
  for (const part of parts) {
72
- if (iconMapping[part]) {
73
- return part;
74
- }
47
+ if (iconMapping[part]) return part;
75
48
  }
76
49
  }
77
50
  }
78
51
 
79
- // Check aria-label
80
52
  const ariaMatch = svgTag.match(/aria-label=["']([^"']*)["']/i);
81
53
  if (ariaMatch) {
82
54
  const label = ariaMatch[1].toLowerCase();
83
55
  for (const keyword of Object.keys(iconMapping)) {
84
- if (label.includes(keyword)) {
85
- return keyword;
86
- }
56
+ if (label.includes(keyword)) return keyword;
87
57
  }
88
58
  }
89
59
 
90
- // Check surrounding context for hints
91
60
  const contextLower = context.toLowerCase();
92
-
93
- // Priority keywords for Japanese business sites
94
61
  const priorityKeywords = [
95
62
  'mail', 'email', 'phone', 'tel', 'location', 'address',
96
63
  'menu', 'search', 'home', 'arrow', 'chevron',
@@ -99,191 +66,78 @@ function detectIconPurpose(svgTag, context) {
99
66
  'check', 'info', 'warning', 'success', 'star',
100
67
  'sakura', 'wave', 'zen'
101
68
  ];
102
-
103
69
  for (const keyword of priorityKeywords) {
104
- if (contextLower.includes(keyword)) {
105
- return keyword;
106
- }
70
+ if (contextLower.includes(keyword)) return keyword;
107
71
  }
108
72
 
109
- // Default to decorative
110
73
  return 'decorative';
111
74
  }
112
75
 
113
76
  /**
114
- * Find SVG elements that need replacement
77
+ * Inject replacement icons into HTML file.
78
+ * @param {string} htmlPath
79
+ * @param {boolean} verbose
80
+ * @returns {Promise<{ success: boolean, replacedCount: number, replacements?: Array }>}
115
81
  */
116
- function findSvgElements(html) {
117
- const elements = [];
118
-
119
- // Pattern 1: Generic SVG with viewBox (likely placeholder)
120
- const svgRegex = /<svg[^>]*viewBox=["'][^"']*["'][^>]*>[\s\S]*?<\/svg>/gi;
82
+ export async function injectIcons(htmlPath, verbose = false) {
83
+ const html = await fs.readFile(htmlPath, 'utf-8');
84
+ const elements = findSvgElements(html, detectIconPurpose);
121
85
 
122
- let match;
123
- while ((match = svgRegex.exec(html)) !== null) {
124
- const svgTag = match[0];
125
- const context = extractContext(html, match.index);
126
-
127
- // Skip if it's a complex SVG (logo, illustration)
128
- // Simple icons typically have fewer than 3 path/shape elements
129
- const pathCount = (svgTag.match(/<(path|circle|rect|line|polyline|polygon)/gi) || []).length;
130
-
131
- // Skip logo SVGs (typically contain text elements or complex paths)
132
- if (svgTag.includes('<text') || pathCount > 6) {
133
- continue;
134
- }
135
-
136
- // Skip if it has specific classes indicating it's a logo
137
- if (/class=["'][^"']*(logo|brand)[^"']*["']/i.test(svgTag)) {
138
- continue;
139
- }
140
-
141
- elements.push({
142
- original: svgTag,
143
- position: match.index,
144
- context: context,
145
- purpose: detectIconPurpose(svgTag, context)
146
- });
147
- }
148
-
149
- return elements;
150
- }
151
-
152
- /**
153
- * Preserve original attributes when replacing SVG
154
- */
155
- function preserveAttributes(originalSvg, newSvg) {
156
- // Extract class from original
157
- const classMatch = originalSvg.match(/class=["']([^"']*)["']/i);
158
- const widthMatch = originalSvg.match(/width=["']([^"']*)["']/i);
159
- const heightMatch = originalSvg.match(/height=["']([^"']*)["']/i);
160
- const ariaMatch = originalSvg.match(/aria-[^=]+=["'][^"']*["']/gi);
161
-
162
- let result = newSvg;
163
-
164
- // Add class if present
165
- if (classMatch) {
166
- result = result.replace('<svg', `<svg class="${classMatch[1]}"`);
167
- }
168
-
169
- // Preserve width/height if specified
170
- if (widthMatch) {
171
- result = result.replace('<svg', `<svg width="${widthMatch[1]}"`);
172
- }
173
- if (heightMatch) {
174
- result = result.replace('<svg', `<svg height="${heightMatch[1]}"`);
175
- }
176
-
177
- // Preserve aria attributes
178
- if (ariaMatch) {
179
- const attrs = ariaMatch.join(' ');
180
- result = result.replace('<svg', `<svg ${attrs}`);
181
- }
182
-
183
- return result;
184
- }
185
-
186
- /**
187
- * Inject icons into HTML
188
- */
189
- async function injectIcons(htmlPath, verbose = false) {
190
- // Read HTML
191
- const html = await fs.readFile(htmlPath, 'utf-8');
192
-
193
- // Find SVG elements
194
- const elements = findSvgElements(html);
195
-
196
- if (verbose) {
197
- console.log(` Found ${elements.length} SVG elements to enhance`);
198
- }
86
+ if (verbose) console.log(` Found ${elements.length} SVG elements to enhance`);
199
87
 
200
88
  if (elements.length === 0) {
201
89
  console.log(' → No SVG icons to enhance');
202
- return {
203
- success: true,
204
- replacedCount: 0
205
- };
90
+ return { success: true, replacedCount: 0 };
206
91
  }
207
92
 
208
- let updatedHtml = html;
93
+ let updatedHtml = html;
209
94
  let replacedCount = 0;
210
95
  const replacements = [];
211
96
 
212
- // Process elements in reverse order to maintain positions
213
- const sortedElements = [...elements].sort((a, b) => b.position - a.position);
97
+ // Process in reverse order to preserve character positions
98
+ const sorted = [...elements].sort((a, b) => b.position - a.position);
214
99
 
215
- for (const element of sortedElements) {
216
- const iconName = iconMapping[element.purpose] || 'decorative-dot';
217
- const newIcon = getIcon(iconName);
100
+ for (const element of sorted) {
101
+ const iconName = iconMapping[element.purpose] || 'decorative-dot';
102
+ const newIcon = getIcon(iconName);
218
103
  const preservedIcon = preserveAttributes(element.original, newIcon);
219
104
 
220
105
  updatedHtml = updatedHtml.replace(element.original, preservedIcon);
221
106
  replacedCount++;
107
+ replacements.push({ purpose: element.purpose, iconName });
222
108
 
223
- replacements.push({
224
- purpose: element.purpose,
225
- iconName: iconName
226
- });
227
-
228
- if (verbose) {
229
- console.log(` → Replaced: ${element.purpose} → ${iconName}`);
230
- }
109
+ if (verbose) console.log(` → Replaced: ${element.purpose} → ${iconName}`);
231
110
  }
232
111
 
233
- // Write updated HTML
234
112
  await fs.writeFile(htmlPath, updatedHtml, 'utf-8');
235
-
236
113
  console.log(` ✓ Enhanced ${replacedCount} icons with Japanese style`);
237
114
 
238
- return {
239
- success: true,
240
- replacedCount,
241
- replacements
242
- };
115
+ return { success: true, replacedCount, replacements };
243
116
  }
244
117
 
245
118
  /**
246
- * Add icon styles to HTML if not present
119
+ * Ensure icon base styles are present in HTML (idempotent).
120
+ * @param {string} htmlPath
247
121
  */
248
- async function ensureIconStyles(htmlPath) {
122
+ export async function ensureIconStyles(htmlPath) {
249
123
  const html = await fs.readFile(htmlPath, 'utf-8');
250
-
251
- // Check if icon styles already exist
252
- if (html.includes('.icon {') || html.includes('/* Icon styles */')) {
253
- return;
254
- }
124
+ if (html.includes('.icon {') || html.includes('/* Icon styles */')) return;
255
125
 
256
126
  const iconStyles = `
257
127
  /* Japanese-style icon defaults */
258
- .icon {
259
- width: 24px;
260
- height: 24px;
261
- flex-shrink: 0;
262
- }
263
-
264
- .icon--sm {
265
- width: 16px;
266
- height: 16px;
267
- }
268
-
269
- .icon--lg {
270
- width: 32px;
271
- height: 32px;
272
- }
273
-
274
- .icon--decorative {
275
- opacity: 0.6;
276
- }
128
+ .icon { width: 24px; height: 24px; flex-shrink: 0; }
129
+ .icon--sm { width: 16px; height: 16px; }
130
+ .icon--lg { width: 32px; height: 32px; }
131
+ .icon--decorative { opacity: 0.6; }
277
132
  `;
278
133
 
279
- // Find </style> or add before </head>
280
134
  let updatedHtml;
281
135
  if (html.includes('</style>')) {
282
136
  updatedHtml = html.replace('</style>', `${iconStyles}\n</style>`);
283
137
  } else if (html.includes('</head>')) {
284
138
  updatedHtml = html.replace('</head>', `<style>${iconStyles}</style>\n</head>`);
285
139
  } else {
286
- return; // Can't safely add styles
140
+ return;
287
141
  }
288
142
 
289
143
  await fs.writeFile(htmlPath, updatedHtml, 'utf-8');
@@ -299,13 +153,6 @@ if (process.argv[1] === new URL(import.meta.url).pathname) {
299
153
  }
300
154
 
301
155
  injectIcons(args.html, args.verbose)
302
- .then(result => {
303
- console.log(JSON.stringify(result, null, 2));
304
- })
305
- .catch(error => {
306
- console.error('Error:', error.message);
307
- process.exit(1);
308
- });
156
+ .then(result => console.log(JSON.stringify(result, null, 2)))
157
+ .catch(error => { console.error('Error:', error.message); process.exit(1); });
309
158
  }
310
-
311
- export { injectIcons, findSvgElements, detectIconPurpose };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Base Discoverer Utilities
3
+ *
4
+ * Route normalization, deduplication, name extraction, and string helpers
5
+ * extracted from base-discoverer.js to keep each file under 200 lines.
6
+ */
7
+
8
+ // Dynamic segment patterns for detecting parameterized routes
9
+ export const DYNAMIC_PATTERNS = [
10
+ /\[[\w-]+\]/, // Next.js [slug]
11
+ /\[\.\.\.([\w-]+)\]/, // Next.js catch-all [...slug]
12
+ /:[\w-]+/, // Vue/React :id
13
+ /\{[\w-]+\}/, // Angular {id}
14
+ /\*[\w-]*/ // Wildcard
15
+ ];
16
+
17
+ // Source priority for deduplication (higher = preferred)
18
+ export const SOURCE_PRIORITY = {
19
+ 'framework': 4,
20
+ 'interception': 3,
21
+ 'sitemap': 2,
22
+ 'link-scrape': 1
23
+ };
24
+
25
+ /**
26
+ * Normalize a route path:
27
+ * - Strips full URL to pathname
28
+ * - Ensures leading slash
29
+ * - Removes query params and hash
30
+ * - Removes trailing slash (except root)
31
+ * @param {string} rawPath
32
+ * @returns {string}
33
+ */
34
+ export function normalizeRoute(rawPath) {
35
+ if (!rawPath || typeof rawPath !== 'string') return '/';
36
+
37
+ let p = rawPath;
38
+
39
+ if (p.startsWith('http')) {
40
+ try {
41
+ p = new URL(p).pathname;
42
+ } catch {
43
+ return '/';
44
+ }
45
+ }
46
+
47
+ if (!p.startsWith('/')) p = '/' + p;
48
+
49
+ p = p.split('?')[0].split('#')[0];
50
+
51
+ if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
52
+
53
+ return p;
54
+ }
55
+
56
+ /**
57
+ * Check if a path contains dynamic segments
58
+ * @param {string} path
59
+ * @returns {boolean}
60
+ */
61
+ export function isDynamicRoute(path) {
62
+ return DYNAMIC_PATTERNS.some(pattern => pattern.test(path));
63
+ }
64
+
65
+ /**
66
+ * Convert string to Title Case (handles kebab-case and snake_case)
67
+ * @param {string} str
68
+ * @returns {string}
69
+ */
70
+ export function titleCase(str) {
71
+ return str
72
+ .replace(/[-_]/g, ' ')
73
+ .replace(/\b\w/g, c => c.toUpperCase());
74
+ }
75
+
76
+ /**
77
+ * Extract a human-readable page name from a route path
78
+ * @param {string} path - Route path
79
+ * @param {string} [componentName] - Optional component name hint
80
+ * @returns {string}
81
+ */
82
+ export function extractPageName(path, componentName) {
83
+ if (componentName && componentName !== 'default' && componentName !== 'index') {
84
+ return componentName
85
+ .replace(/([A-Z])/g, ' $1')
86
+ .replace(/^./, s => s.toUpperCase())
87
+ .trim();
88
+ }
89
+
90
+ const normalized = normalizeRoute(path);
91
+ if (normalized === '/') return 'Home';
92
+
93
+ const segments = normalized.split('/').filter(Boolean);
94
+ if (segments.length === 0) return 'Home';
95
+
96
+ let lastSegment = segments[segments.length - 1];
97
+
98
+ if (isDynamicRoute(lastSegment)) {
99
+ lastSegment = lastSegment.replace(/[\[\]:{}*\.]/g, '');
100
+ return `${titleCase(lastSegment)} (Dynamic)`;
101
+ }
102
+
103
+ return titleCase(lastSegment);
104
+ }
105
+
106
+ /**
107
+ * Deduplicate routes by path, preferring higher-priority sources.
108
+ * @param {Array} routes - Raw route objects
109
+ * @param {string} baseOrigin - Origin for building full URLs
110
+ * @returns {Array} Deduplicated routes with normalized paths and full URLs
111
+ */
112
+ export function deduplicateRoutes(routes, baseOrigin) {
113
+ const seen = new Map();
114
+
115
+ for (const route of routes) {
116
+ const normalized = normalizeRoute(route.path);
117
+ const existing = seen.get(normalized);
118
+
119
+ const currentPriority = SOURCE_PRIORITY[route.source] || 0;
120
+ const existingPriority = existing ? (SOURCE_PRIORITY[existing.source] || 0) : -1;
121
+
122
+ const shouldReplace = !existing ||
123
+ currentPriority > existingPriority ||
124
+ (currentPriority === existingPriority && route.name && !existing.name);
125
+
126
+ if (shouldReplace) {
127
+ seen.set(normalized, {
128
+ ...route,
129
+ path: normalized,
130
+ url: `${baseOrigin}${normalized}`,
131
+ dynamic: isDynamicRoute(normalized)
132
+ });
133
+ }
134
+ }
135
+
136
+ return Array.from(seen.values());
137
+ }