real-prototypes-skill 0.1.0 → 0.1.2

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.
@@ -0,0 +1,557 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CSS Extraction Module
5
+ *
6
+ * Extracts styles from captured HTML files to understand the
7
+ * styling approach used by the original platform.
8
+ *
9
+ * Features:
10
+ * - Parses <style> tags from HTML
11
+ * - Extracts inline styles from elements
12
+ * - Identifies styling paradigm (CSS modules, Tailwind, SLDS, etc.)
13
+ * - Generates CSS module files
14
+ * - Maps CSS classes to their definitions
15
+ *
16
+ * Usage:
17
+ * node extract-css.js --project <name> --page <page>
18
+ * node extract-css.js --input ./html/page.html --output ./styles
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { JSDOM } = require('jsdom');
24
+
25
+ // Common CSS frameworks/libraries patterns
26
+ const FRAMEWORK_PATTERNS = {
27
+ tailwind: {
28
+ patterns: [
29
+ /\b(flex|grid|block|inline|hidden)\b/,
30
+ /\b(p|m|w|h|gap|space)-(\d+|auto|full|screen|px)\b/,
31
+ /\b(bg|text|border)-(gray|blue|red|green|yellow|purple|pink|indigo|white|black|transparent)-?\d*/,
32
+ /\btext-(xs|sm|base|lg|xl|2xl|3xl)\b/,
33
+ /\b(rounded|shadow|opacity|transition|duration)-?\w*/
34
+ ],
35
+ confidence: 0
36
+ },
37
+ slds: {
38
+ patterns: [
39
+ /\bslds-[\w-]+/,
40
+ /\bsf-[\w-]+/
41
+ ],
42
+ confidence: 0
43
+ },
44
+ bootstrap: {
45
+ patterns: [
46
+ /\bbtn-[\w-]+/,
47
+ /\bcol-[\w-]+/,
48
+ /\brow\b/,
49
+ /\bcontainer(-fluid)?\b/,
50
+ /\bcard-[\w-]+/,
51
+ /\bform-[\w-]+/
52
+ ],
53
+ confidence: 0
54
+ },
55
+ materialUI: {
56
+ patterns: [
57
+ /\bMui[\w-]+/,
58
+ /\bmdc-[\w-]+/
59
+ ],
60
+ confidence: 0
61
+ },
62
+ antd: {
63
+ patterns: [
64
+ /\bant-[\w-]+/
65
+ ],
66
+ confidence: 0
67
+ },
68
+ custom: {
69
+ patterns: [],
70
+ confidence: 0
71
+ }
72
+ };
73
+
74
+ class CSSExtractor {
75
+ constructor(options = {}) {
76
+ this.options = {
77
+ generateModules: true,
78
+ analyzeFramework: true,
79
+ extractInline: true,
80
+ ...options
81
+ };
82
+
83
+ this.dom = null;
84
+ this.document = null;
85
+ this.styleSheets = [];
86
+ this.inlineStyles = [];
87
+ this.classUsage = new Map();
88
+ this.detectedFramework = null;
89
+ }
90
+
91
+ /**
92
+ * Load HTML from file
93
+ */
94
+ loadFromFile(htmlPath) {
95
+ if (!fs.existsSync(htmlPath)) {
96
+ throw new Error(`HTML file not found: ${htmlPath}`);
97
+ }
98
+ const html = fs.readFileSync(htmlPath, 'utf-8');
99
+ return this.loadFromString(html);
100
+ }
101
+
102
+ /**
103
+ * Load HTML from string
104
+ */
105
+ loadFromString(html) {
106
+ this.dom = new JSDOM(html);
107
+ this.document = this.dom.window.document;
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * Extract all <style> tags
113
+ */
114
+ extractStyleTags() {
115
+ const styleTags = this.document.querySelectorAll('style');
116
+ this.styleSheets = [];
117
+
118
+ for (const style of styleTags) {
119
+ const css = style.textContent.trim();
120
+ if (css) {
121
+ this.styleSheets.push({
122
+ type: 'embedded',
123
+ content: css,
124
+ rules: this.parseCSS(css)
125
+ });
126
+ }
127
+ }
128
+
129
+ return this.styleSheets;
130
+ }
131
+
132
+ /**
133
+ * Parse CSS string into rules
134
+ */
135
+ parseCSS(css) {
136
+ const rules = [];
137
+
138
+ // Remove comments
139
+ css = css.replace(/\/\*[\s\S]*?\*\//g, '');
140
+
141
+ // Simple regex-based parser (not comprehensive but works for most cases)
142
+ const rulePattern = /([^{}]+)\{([^{}]+)\}/g;
143
+ let match;
144
+
145
+ while ((match = rulePattern.exec(css)) !== null) {
146
+ const selector = match[1].trim();
147
+ const declarations = match[2].trim();
148
+
149
+ const properties = {};
150
+ const propPattern = /([\w-]+)\s*:\s*([^;]+)/g;
151
+ let propMatch;
152
+
153
+ while ((propMatch = propPattern.exec(declarations)) !== null) {
154
+ properties[propMatch[1].trim()] = propMatch[2].trim();
155
+ }
156
+
157
+ rules.push({
158
+ selector,
159
+ properties,
160
+ raw: `${selector} { ${declarations} }`
161
+ });
162
+ }
163
+
164
+ return rules;
165
+ }
166
+
167
+ /**
168
+ * Extract inline styles from all elements
169
+ */
170
+ extractInlineStyles() {
171
+ const elements = this.document.querySelectorAll('[style]');
172
+ this.inlineStyles = [];
173
+
174
+ for (const el of elements) {
175
+ const styleAttr = el.getAttribute('style');
176
+ if (styleAttr) {
177
+ const properties = this.parseInlineStyle(styleAttr);
178
+ const tagName = el.tagName.toLowerCase();
179
+ const className = el.className || '';
180
+ const id = el.id || '';
181
+
182
+ this.inlineStyles.push({
183
+ element: tagName,
184
+ className,
185
+ id,
186
+ style: styleAttr,
187
+ properties,
188
+ selector: this.generateSelector(el)
189
+ });
190
+ }
191
+ }
192
+
193
+ return this.inlineStyles;
194
+ }
195
+
196
+ /**
197
+ * Parse inline style string to object
198
+ */
199
+ parseInlineStyle(styleStr) {
200
+ const properties = {};
201
+ const rules = styleStr.split(';').filter(Boolean);
202
+
203
+ for (const rule of rules) {
204
+ const [property, value] = rule.split(':').map(s => s.trim());
205
+ if (property && value) {
206
+ properties[property] = value;
207
+ }
208
+ }
209
+
210
+ return properties;
211
+ }
212
+
213
+ /**
214
+ * Generate CSS selector for element
215
+ */
216
+ generateSelector(el) {
217
+ const tagName = el.tagName.toLowerCase();
218
+
219
+ if (el.id) {
220
+ return `#${el.id}`;
221
+ }
222
+
223
+ if (el.className && typeof el.className === 'string') {
224
+ const firstClass = el.className.split(' ')[0];
225
+ return `.${firstClass}`;
226
+ }
227
+
228
+ return tagName;
229
+ }
230
+
231
+ /**
232
+ * Analyze class usage across all elements
233
+ */
234
+ analyzeClassUsage() {
235
+ const elements = this.document.querySelectorAll('[class]');
236
+
237
+ for (const el of elements) {
238
+ const className = el.className;
239
+ if (typeof className !== 'string') continue;
240
+
241
+ const classes = className.split(/\s+/).filter(Boolean);
242
+
243
+ for (const cls of classes) {
244
+ if (!this.classUsage.has(cls)) {
245
+ this.classUsage.set(cls, { count: 0, elements: [] });
246
+ }
247
+
248
+ const usage = this.classUsage.get(cls);
249
+ usage.count++;
250
+ if (usage.elements.length < 3) {
251
+ usage.elements.push(el.tagName.toLowerCase());
252
+ }
253
+ }
254
+ }
255
+
256
+ return this.classUsage;
257
+ }
258
+
259
+ /**
260
+ * Detect CSS framework/library used
261
+ */
262
+ detectFramework() {
263
+ this.analyzeClassUsage();
264
+
265
+ const allClasses = Array.from(this.classUsage.keys()).join(' ');
266
+
267
+ for (const [name, config] of Object.entries(FRAMEWORK_PATTERNS)) {
268
+ let matches = 0;
269
+ for (const pattern of config.patterns) {
270
+ const matchResult = allClasses.match(pattern);
271
+ if (matchResult) {
272
+ matches += matchResult.length;
273
+ }
274
+ }
275
+ config.confidence = matches;
276
+ }
277
+
278
+ // Find framework with highest confidence
279
+ let bestFramework = 'custom';
280
+ let bestConfidence = 0;
281
+
282
+ for (const [name, config] of Object.entries(FRAMEWORK_PATTERNS)) {
283
+ if (config.confidence > bestConfidence) {
284
+ bestFramework = name;
285
+ bestConfidence = config.confidence;
286
+ }
287
+ }
288
+
289
+ this.detectedFramework = {
290
+ name: bestFramework,
291
+ confidence: bestConfidence,
292
+ allScores: Object.fromEntries(
293
+ Object.entries(FRAMEWORK_PATTERNS).map(([k, v]) => [k, v.confidence])
294
+ )
295
+ };
296
+
297
+ return this.detectedFramework;
298
+ }
299
+
300
+ /**
301
+ * Generate CSS module from extracted styles
302
+ */
303
+ generateCSSModule() {
304
+ const moduleContent = [];
305
+
306
+ // Add extracted stylesheet rules
307
+ for (const sheet of this.styleSheets) {
308
+ for (const rule of sheet.rules) {
309
+ moduleContent.push(rule.raw);
310
+ moduleContent.push('');
311
+ }
312
+ }
313
+
314
+ // Add inline styles as classes
315
+ if (this.inlineStyles.length > 0) {
316
+ moduleContent.push('/* Inline styles extracted as classes */');
317
+
318
+ for (let i = 0; i < this.inlineStyles.length; i++) {
319
+ const style = this.inlineStyles[i];
320
+ const className = style.className ?
321
+ style.className.split(' ')[0] :
322
+ `inline-${i}`;
323
+
324
+ const declarations = Object.entries(style.properties)
325
+ .map(([prop, val]) => ` ${prop}: ${val};`)
326
+ .join('\n');
327
+
328
+ moduleContent.push(`.${className} {`);
329
+ moduleContent.push(declarations);
330
+ moduleContent.push('}');
331
+ moduleContent.push('');
332
+ }
333
+ }
334
+
335
+ return moduleContent.join('\n');
336
+ }
337
+
338
+ /**
339
+ * Get most used classes
340
+ */
341
+ getMostUsedClasses(limit = 20) {
342
+ const sorted = Array.from(this.classUsage.entries())
343
+ .sort((a, b) => b[1].count - a[1].count)
344
+ .slice(0, limit);
345
+
346
+ return sorted.map(([cls, usage]) => ({
347
+ class: cls,
348
+ count: usage.count,
349
+ elements: usage.elements
350
+ }));
351
+ }
352
+
353
+ /**
354
+ * Get styling recommendation
355
+ */
356
+ getStylingRecommendation() {
357
+ const framework = this.detectFramework();
358
+
359
+ const recommendations = {
360
+ tailwind: 'Use Tailwind utility classes. Match exact class combinations.',
361
+ slds: 'Use Salesforce Lightning Design System (SLDS) classes. Import @salesforce-ux/design-system.',
362
+ bootstrap: 'Use Bootstrap classes. Import bootstrap CSS.',
363
+ materialUI: 'Use Material-UI components and styling. Import @mui/material.',
364
+ antd: 'Use Ant Design components. Import antd.',
365
+ custom: 'Use CSS modules or inline styles to match the custom styling.'
366
+ };
367
+
368
+ return {
369
+ framework: framework.name,
370
+ confidence: framework.confidence,
371
+ recommendation: recommendations[framework.name] || recommendations.custom,
372
+ approach: framework.confidence > 10 ? 'framework' : 'inline-styles'
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Extract all data
378
+ */
379
+ extract() {
380
+ this.extractStyleTags();
381
+ this.extractInlineStyles();
382
+ this.analyzeClassUsage();
383
+ this.detectFramework();
384
+
385
+ return {
386
+ styleSheets: this.styleSheets,
387
+ inlineStyles: this.inlineStyles,
388
+ classUsage: Object.fromEntries(this.classUsage),
389
+ framework: this.detectedFramework,
390
+ recommendation: this.getStylingRecommendation(),
391
+ mostUsedClasses: this.getMostUsedClasses()
392
+ };
393
+ }
394
+
395
+ /**
396
+ * Format results for CLI output
397
+ */
398
+ formatResults() {
399
+ const lines = [];
400
+ const data = this.extract();
401
+
402
+ lines.push('\x1b[1mCSS Extraction Results\x1b[0m\n');
403
+
404
+ // Framework detection
405
+ lines.push('\x1b[1mDetected Framework:\x1b[0m');
406
+ lines.push(` ${data.framework.name} (confidence: ${data.framework.confidence})`);
407
+ lines.push('');
408
+
409
+ // Recommendation
410
+ lines.push('\x1b[1mRecommendation:\x1b[0m');
411
+ lines.push(` ${data.recommendation.recommendation}`);
412
+ lines.push(` Approach: ${data.recommendation.approach}`);
413
+ lines.push('');
414
+
415
+ // Style sheets
416
+ lines.push(`\x1b[1mEmbedded Style Tags:\x1b[0m ${data.styleSheets.length}`);
417
+ for (const sheet of data.styleSheets.slice(0, 3)) {
418
+ lines.push(` ${sheet.rules.length} rules`);
419
+ }
420
+ lines.push('');
421
+
422
+ // Inline styles
423
+ lines.push(`\x1b[1mInline Styles:\x1b[0m ${data.inlineStyles.length}`);
424
+ lines.push('');
425
+
426
+ // Most used classes
427
+ lines.push('\x1b[1mMost Used Classes:\x1b[0m');
428
+ for (const cls of data.mostUsedClasses.slice(0, 10)) {
429
+ lines.push(` ${cls.class} (${cls.count}x)`);
430
+ }
431
+
432
+ return lines.join('\n');
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Extract CSS from HTML file
438
+ */
439
+ function extractCSS(htmlPath) {
440
+ const extractor = new CSSExtractor();
441
+ extractor.loadFromFile(htmlPath);
442
+ return extractor.extract();
443
+ }
444
+
445
+ /**
446
+ * Write CSS module to file
447
+ */
448
+ function writeCSSModule(extractor, outputPath) {
449
+ const content = extractor.generateCSSModule();
450
+ const dir = path.dirname(outputPath);
451
+
452
+ if (!fs.existsSync(dir)) {
453
+ fs.mkdirSync(dir, { recursive: true });
454
+ }
455
+
456
+ fs.writeFileSync(outputPath, content);
457
+ return outputPath;
458
+ }
459
+
460
+ // CLI execution
461
+ if (require.main === module) {
462
+ const args = process.argv.slice(2);
463
+ let inputPath = null;
464
+ let outputPath = null;
465
+ let projectName = null;
466
+ let pageName = null;
467
+ let jsonOutput = false;
468
+
469
+ for (let i = 0; i < args.length; i++) {
470
+ switch (args[i]) {
471
+ case '--input':
472
+ case '-i':
473
+ inputPath = args[++i];
474
+ break;
475
+ case '--output':
476
+ case '-o':
477
+ outputPath = args[++i];
478
+ break;
479
+ case '--project':
480
+ projectName = args[++i];
481
+ break;
482
+ case '--page':
483
+ pageName = args[++i];
484
+ break;
485
+ case '--json':
486
+ jsonOutput = true;
487
+ break;
488
+ case '--help':
489
+ case '-h':
490
+ console.log(`
491
+ Usage: node extract-css.js [options]
492
+
493
+ Options:
494
+ --input, -i <path> Path to HTML file
495
+ --output, -o <path> Output path for CSS module
496
+ --project <name> Project name
497
+ --page <name> Page name (used with --project)
498
+ --json Output as JSON
499
+ --help, -h Show this help
500
+
501
+ Examples:
502
+ node extract-css.js --input ./html/page.html
503
+ node extract-css.js --project my-app --page homepage
504
+ node extract-css.js -i ./page.html -o ./styles/page.module.css
505
+ `);
506
+ process.exit(0);
507
+ }
508
+ }
509
+
510
+ // Handle project-based paths
511
+ if (projectName) {
512
+ const SKILL_DIR = path.dirname(__dirname);
513
+ const PROJECTS_DIR = path.resolve(SKILL_DIR, '../../../projects');
514
+ const projectDir = path.join(PROJECTS_DIR, projectName);
515
+
516
+ if (pageName) {
517
+ inputPath = path.join(projectDir, 'references', 'html', `${pageName}.html`);
518
+ outputPath = outputPath || path.join(projectDir, 'prototype', 'src', 'styles', `${pageName}.module.css`);
519
+ }
520
+ }
521
+
522
+ if (!inputPath) {
523
+ console.error('\x1b[31mError:\x1b[0m --input is required');
524
+ process.exit(1);
525
+ }
526
+
527
+ try {
528
+ const extractor = new CSSExtractor();
529
+ extractor.loadFromFile(inputPath);
530
+
531
+ if (jsonOutput) {
532
+ console.log(JSON.stringify(extractor.extract(), null, 2));
533
+ } else {
534
+ console.log(`\nInput: ${inputPath}`);
535
+ if (outputPath) {
536
+ console.log(`Output: ${outputPath}`);
537
+ }
538
+ console.log('');
539
+ console.log(extractor.formatResults());
540
+
541
+ if (outputPath) {
542
+ writeCSSModule(extractor, outputPath);
543
+ console.log(`\n\x1b[32m✓ CSS module written to: ${outputPath}\x1b[0m`);
544
+ }
545
+ }
546
+
547
+ } catch (error) {
548
+ console.error(`\x1b[31mError:\x1b[0m ${error.message}`);
549
+ process.exit(1);
550
+ }
551
+ }
552
+
553
+ module.exports = {
554
+ CSSExtractor,
555
+ extractCSS,
556
+ writeCSSModule
557
+ };