real-prototypes-skill 0.1.1 → 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,731 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Component Library Extractor
5
+ *
6
+ * Analyzes captured HTML files to identify common UI patterns and
7
+ * generates a reusable component library from the captured platform.
8
+ *
9
+ * Features:
10
+ * - Identifies common UI patterns (buttons, cards, inputs, tables)
11
+ * - Extracts component variants (primary, secondary, destructive)
12
+ * - Generates React component files with exact styling
13
+ * - Creates component registry for easy lookup
14
+ * - Tracks component source for reference
15
+ *
16
+ * Usage:
17
+ * node extract-components.js --project <name>
18
+ * node extract-components.js --input ./html --output ./components
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { JSDOM } = require('jsdom');
24
+
25
+ // Component detection patterns
26
+ const COMPONENT_PATTERNS = {
27
+ Button: {
28
+ selectors: [
29
+ 'button',
30
+ '[role="button"]',
31
+ 'a.btn',
32
+ 'a.button',
33
+ '[class*="btn"]',
34
+ '[class*="button"]'
35
+ ],
36
+ excludeSelectors: ['[type="submit"]', '[type="reset"]'],
37
+ variants: {
38
+ primary: [/primary/i, /btn-primary/i, /bg-blue/i, /bg-primary/i],
39
+ secondary: [/secondary/i, /btn-secondary/i, /btn-outline/i, /btn-ghost/i],
40
+ destructive: [/danger/i, /destructive/i, /btn-danger/i, /btn-red/i, /delete/i],
41
+ disabled: [/disabled/i, /btn-disabled/i]
42
+ }
43
+ },
44
+ Card: {
45
+ selectors: [
46
+ '[class*="card"]',
47
+ '[class*="panel"]',
48
+ '[class*="tile"]',
49
+ '[class*="box"]',
50
+ 'article'
51
+ ],
52
+ variants: {
53
+ default: [/card(?!-)/i],
54
+ elevated: [/shadow/i, /elevated/i],
55
+ bordered: [/border/i, /outlined/i]
56
+ }
57
+ },
58
+ Input: {
59
+ selectors: [
60
+ 'input[type="text"]',
61
+ 'input[type="email"]',
62
+ 'input[type="password"]',
63
+ 'input[type="search"]',
64
+ 'input[type="tel"]',
65
+ 'input[type="url"]',
66
+ 'input:not([type])',
67
+ 'textarea',
68
+ '[class*="input"]',
69
+ '[class*="text-field"]'
70
+ ],
71
+ variants: {
72
+ default: [/input(?!-)/i],
73
+ error: [/error/i, /invalid/i, /danger/i],
74
+ success: [/success/i, /valid/i],
75
+ disabled: [/disabled/i]
76
+ }
77
+ },
78
+ Select: {
79
+ selectors: [
80
+ 'select',
81
+ '[class*="select"]',
82
+ '[class*="dropdown"]',
83
+ '[role="listbox"]'
84
+ ],
85
+ variants: {
86
+ default: [/select(?!-)/i],
87
+ multiple: [/multiple/i]
88
+ }
89
+ },
90
+ Table: {
91
+ selectors: [
92
+ 'table',
93
+ '[class*="table"]',
94
+ '[class*="data-grid"]',
95
+ '[role="grid"]'
96
+ ],
97
+ variants: {
98
+ default: [/table(?!-)/i],
99
+ striped: [/striped/i, /zebra/i],
100
+ bordered: [/bordered/i]
101
+ }
102
+ },
103
+ Badge: {
104
+ selectors: [
105
+ '[class*="badge"]',
106
+ '[class*="tag"]',
107
+ '[class*="chip"]',
108
+ '[class*="label"]',
109
+ '[class*="pill"]'
110
+ ],
111
+ variants: {
112
+ default: [/badge(?!-)/i],
113
+ success: [/success/i, /green/i],
114
+ warning: [/warning/i, /yellow/i, /orange/i],
115
+ error: [/error/i, /danger/i, /red/i],
116
+ info: [/info/i, /blue/i]
117
+ }
118
+ },
119
+ Avatar: {
120
+ selectors: [
121
+ '[class*="avatar"]',
122
+ '[class*="profile-image"]',
123
+ '[class*="user-image"]',
124
+ 'img[class*="rounded-full"]'
125
+ ],
126
+ variants: {
127
+ default: [/avatar(?!-)/i],
128
+ small: [/sm/i, /small/i, /xs/i],
129
+ large: [/lg/i, /large/i, /xl/i]
130
+ }
131
+ },
132
+ Modal: {
133
+ selectors: [
134
+ '[class*="modal"]',
135
+ '[class*="dialog"]',
136
+ '[role="dialog"]',
137
+ '[aria-modal="true"]'
138
+ ],
139
+ variants: {
140
+ default: [/modal(?!-)/i]
141
+ }
142
+ },
143
+ Alert: {
144
+ selectors: [
145
+ '[class*="alert"]',
146
+ '[class*="notification"]',
147
+ '[class*="toast"]',
148
+ '[role="alert"]'
149
+ ],
150
+ variants: {
151
+ success: [/success/i, /green/i],
152
+ warning: [/warning/i, /yellow/i],
153
+ error: [/error/i, /danger/i, /red/i],
154
+ info: [/info/i, /blue/i]
155
+ }
156
+ },
157
+ Tabs: {
158
+ selectors: [
159
+ '[class*="tabs"]',
160
+ '[role="tablist"]',
161
+ '[class*="tab-list"]'
162
+ ],
163
+ variants: {
164
+ default: [/tabs(?!-)/i]
165
+ }
166
+ },
167
+ Navigation: {
168
+ selectors: [
169
+ 'nav',
170
+ '[class*="nav"]',
171
+ '[class*="sidebar"]',
172
+ '[class*="menu"]',
173
+ '[role="navigation"]'
174
+ ],
175
+ variants: {
176
+ horizontal: [/horizontal/i, /navbar/i, /topnav/i],
177
+ vertical: [/vertical/i, /sidebar/i, /sidenav/i]
178
+ }
179
+ }
180
+ };
181
+
182
+ class ComponentExtractor {
183
+ constructor(options = {}) {
184
+ this.options = {
185
+ minOccurrences: 1,
186
+ generateTypeScript: true,
187
+ ...options
188
+ };
189
+
190
+ this.components = new Map();
191
+ this.registry = {
192
+ version: '1.0.0',
193
+ generatedAt: new Date().toISOString(),
194
+ components: {}
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Load and analyze an HTML file
200
+ */
201
+ analyzeFile(htmlPath) {
202
+ if (!fs.existsSync(htmlPath)) {
203
+ throw new Error(`HTML file not found: ${htmlPath}`);
204
+ }
205
+
206
+ const html = fs.readFileSync(htmlPath, 'utf-8');
207
+ const dom = new JSDOM(html);
208
+ const document = dom.window.document;
209
+ const fileName = path.basename(htmlPath, '.html');
210
+
211
+ for (const [componentType, config] of Object.entries(COMPONENT_PATTERNS)) {
212
+ this.extractComponent(document, componentType, config, fileName);
213
+ }
214
+
215
+ return this;
216
+ }
217
+
218
+ /**
219
+ * Analyze multiple HTML files in a directory
220
+ */
221
+ analyzeDirectory(htmlDir) {
222
+ if (!fs.existsSync(htmlDir)) {
223
+ throw new Error(`Directory not found: ${htmlDir}`);
224
+ }
225
+
226
+ const files = fs.readdirSync(htmlDir)
227
+ .filter(f => f.endsWith('.html'));
228
+
229
+ for (const file of files) {
230
+ this.analyzeFile(path.join(htmlDir, file));
231
+ }
232
+
233
+ return this;
234
+ }
235
+
236
+ /**
237
+ * Extract a specific component type from document
238
+ */
239
+ extractComponent(document, componentType, config, sourceFile) {
240
+ for (const selector of config.selectors) {
241
+ try {
242
+ const elements = document.querySelectorAll(selector);
243
+
244
+ for (const element of elements) {
245
+ // Skip if matches exclude selector
246
+ if (config.excludeSelectors) {
247
+ const shouldExclude = config.excludeSelectors.some(excl => {
248
+ try {
249
+ return element.matches(excl);
250
+ } catch {
251
+ return false;
252
+ }
253
+ });
254
+ if (shouldExclude) continue;
255
+ }
256
+
257
+ // Determine variant
258
+ const variant = this.detectVariant(element, config.variants);
259
+
260
+ // Extract component data
261
+ const componentData = {
262
+ type: componentType,
263
+ variant,
264
+ className: element.className || '',
265
+ tagName: element.tagName.toLowerCase(),
266
+ html: element.outerHTML.substring(0, 500), // Limit HTML size
267
+ styles: this.extractStyles(element),
268
+ attributes: this.extractAttributes(element),
269
+ sourceFile,
270
+ selector
271
+ };
272
+
273
+ // Add to components map
274
+ const key = `${componentType}-${variant}`;
275
+ if (!this.components.has(key)) {
276
+ this.components.set(key, []);
277
+ }
278
+ this.components.get(key).push(componentData);
279
+ }
280
+ } catch (e) {
281
+ // Invalid selector, skip
282
+ }
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Detect variant from element classes
288
+ */
289
+ detectVariant(element, variants) {
290
+ const className = element.className || '';
291
+
292
+ for (const [variant, patterns] of Object.entries(variants)) {
293
+ for (const pattern of patterns) {
294
+ if (pattern.test(className)) {
295
+ return variant;
296
+ }
297
+ }
298
+ }
299
+
300
+ return 'default';
301
+ }
302
+
303
+ /**
304
+ * Extract inline styles
305
+ */
306
+ extractStyles(element) {
307
+ const styleAttr = element.getAttribute('style');
308
+ if (!styleAttr) return {};
309
+
310
+ const styles = {};
311
+ const rules = styleAttr.split(';').filter(Boolean);
312
+
313
+ for (const rule of rules) {
314
+ const [prop, value] = rule.split(':').map(s => s.trim());
315
+ if (prop && value) {
316
+ const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
317
+ styles[camelProp] = value;
318
+ }
319
+ }
320
+
321
+ return styles;
322
+ }
323
+
324
+ /**
325
+ * Extract relevant attributes
326
+ */
327
+ extractAttributes(element) {
328
+ const attrs = {};
329
+ const relevantAttrs = ['type', 'role', 'aria-label', 'placeholder', 'disabled'];
330
+
331
+ for (const attr of relevantAttrs) {
332
+ const value = element.getAttribute(attr);
333
+ if (value !== null) {
334
+ attrs[attr] = value;
335
+ }
336
+ }
337
+
338
+ return attrs;
339
+ }
340
+
341
+ /**
342
+ * Generate React component file
343
+ */
344
+ generateComponent(componentType, variants) {
345
+ const ext = this.options.generateTypeScript ? 'tsx' : 'jsx';
346
+ const propsType = this.options.generateTypeScript ? 'Props' : '';
347
+
348
+ // Get unique classes across all variants
349
+ const allClasses = new Set();
350
+ const variantClasses = {};
351
+
352
+ for (const [key, instances] of this.components.entries()) {
353
+ if (!key.startsWith(componentType)) continue;
354
+
355
+ const variant = key.split('-').slice(1).join('-');
356
+ variantClasses[variant] = new Set();
357
+
358
+ for (const instance of instances) {
359
+ const classes = instance.className.split(/\s+/).filter(Boolean);
360
+ classes.forEach(c => {
361
+ allClasses.add(c);
362
+ variantClasses[variant].add(c);
363
+ });
364
+ }
365
+ }
366
+
367
+ // Get most common base classes
368
+ const baseClasses = this.getMostCommonClasses(componentType);
369
+
370
+ // Generate component
371
+ const variantNames = Object.keys(variantClasses).filter(v => variantClasses[v].size > 0);
372
+
373
+ let component = '';
374
+
375
+ if (this.options.generateTypeScript) {
376
+ component += `import React from 'react';\n\n`;
377
+
378
+ // Generate props interface
379
+ component += `interface ${componentType}Props {\n`;
380
+ component += ` variant?: ${variantNames.map(v => `'${v}'`).join(' | ') || "'default'"};\n`;
381
+ component += ` className?: string;\n`;
382
+ component += ` children?: React.ReactNode;\n`;
383
+
384
+ // Add specific props based on component type
385
+ if (componentType === 'Button') {
386
+ component += ` onClick?: () => void;\n`;
387
+ component += ` disabled?: boolean;\n`;
388
+ component += ` type?: 'button' | 'submit' | 'reset';\n`;
389
+ } else if (componentType === 'Input') {
390
+ component += ` value?: string;\n`;
391
+ component += ` onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;\n`;
392
+ component += ` placeholder?: string;\n`;
393
+ component += ` disabled?: boolean;\n`;
394
+ }
395
+
396
+ component += `}\n\n`;
397
+ }
398
+
399
+ // Generate variant classes map
400
+ component += `const variantClasses = {\n`;
401
+ for (const [variant, classes] of Object.entries(variantClasses)) {
402
+ const classArray = Array.from(classes).slice(0, 10);
403
+ component += ` ${variant}: '${classArray.join(' ')}',\n`;
404
+ }
405
+ component += `};\n\n`;
406
+
407
+ // Generate component function
408
+ const funcDef = this.options.generateTypeScript
409
+ ? `export const ${componentType}: React.FC<${componentType}Props>`
410
+ : `export const ${componentType}`;
411
+
412
+ component += `${funcDef} = ({\n`;
413
+ component += ` variant = 'default',\n`;
414
+ component += ` className = '',\n`;
415
+ component += ` children,\n`;
416
+
417
+ if (componentType === 'Button') {
418
+ component += ` onClick,\n`;
419
+ component += ` disabled = false,\n`;
420
+ component += ` type = 'button',\n`;
421
+ } else if (componentType === 'Input') {
422
+ component += ` value,\n`;
423
+ component += ` onChange,\n`;
424
+ component += ` placeholder,\n`;
425
+ component += ` disabled = false,\n`;
426
+ }
427
+
428
+ component += ` ...props\n`;
429
+ component += `}) => {\n`;
430
+ component += ` const baseClasses = '${baseClasses.join(' ')}';\n`;
431
+ component += ` const variantClass = variantClasses[variant] || variantClasses.default;\n`;
432
+ component += ` const combinedClasses = \`\${baseClasses} \${variantClass} \${className}\`.trim();\n\n`;
433
+
434
+ // Generate JSX based on component type
435
+ if (componentType === 'Button') {
436
+ component += ` return (\n`;
437
+ component += ` <button\n`;
438
+ component += ` type={type}\n`;
439
+ component += ` className={combinedClasses}\n`;
440
+ component += ` onClick={onClick}\n`;
441
+ component += ` disabled={disabled}\n`;
442
+ component += ` {...props}\n`;
443
+ component += ` >\n`;
444
+ component += ` {children}\n`;
445
+ component += ` </button>\n`;
446
+ component += ` );\n`;
447
+ } else if (componentType === 'Input') {
448
+ component += ` return (\n`;
449
+ component += ` <input\n`;
450
+ component += ` className={combinedClasses}\n`;
451
+ component += ` value={value}\n`;
452
+ component += ` onChange={onChange}\n`;
453
+ component += ` placeholder={placeholder}\n`;
454
+ component += ` disabled={disabled}\n`;
455
+ component += ` {...props}\n`;
456
+ component += ` />\n`;
457
+ component += ` );\n`;
458
+ } else if (componentType === 'Card') {
459
+ component += ` return (\n`;
460
+ component += ` <div className={combinedClasses} {...props}>\n`;
461
+ component += ` {children}\n`;
462
+ component += ` </div>\n`;
463
+ component += ` );\n`;
464
+ } else {
465
+ component += ` return (\n`;
466
+ component += ` <div className={combinedClasses} {...props}>\n`;
467
+ component += ` {children}\n`;
468
+ component += ` </div>\n`;
469
+ component += ` );\n`;
470
+ }
471
+
472
+ component += `};\n\n`;
473
+ component += `export default ${componentType};\n`;
474
+
475
+ return {
476
+ name: componentType,
477
+ filename: `${componentType}.${ext}`,
478
+ content: component,
479
+ variants: variantNames
480
+ };
481
+ }
482
+
483
+ /**
484
+ * Get most common classes for a component type
485
+ */
486
+ getMostCommonClasses(componentType) {
487
+ const classCounts = new Map();
488
+
489
+ for (const [key, instances] of this.components.entries()) {
490
+ if (!key.startsWith(componentType)) continue;
491
+
492
+ for (const instance of instances) {
493
+ const classes = instance.className.split(/\s+/).filter(Boolean);
494
+ for (const cls of classes) {
495
+ classCounts.set(cls, (classCounts.get(cls) || 0) + 1);
496
+ }
497
+ }
498
+ }
499
+
500
+ // Get classes that appear in most instances
501
+ const sorted = Array.from(classCounts.entries())
502
+ .sort((a, b) => b[1] - a[1])
503
+ .slice(0, 5)
504
+ .map(([cls]) => cls);
505
+
506
+ return sorted;
507
+ }
508
+
509
+ /**
510
+ * Generate all components
511
+ */
512
+ generateAll() {
513
+ const generated = [];
514
+ const componentTypes = new Set();
515
+
516
+ // Get unique component types
517
+ for (const key of this.components.keys()) {
518
+ const type = key.split('-')[0];
519
+ componentTypes.add(type);
520
+ }
521
+
522
+ // Generate each component
523
+ for (const type of componentTypes) {
524
+ const variants = {};
525
+ for (const [key, instances] of this.components.entries()) {
526
+ if (key.startsWith(type)) {
527
+ const variant = key.split('-').slice(1).join('-');
528
+ variants[variant] = instances;
529
+ }
530
+ }
531
+
532
+ if (Object.keys(variants).length > 0) {
533
+ const component = this.generateComponent(type, variants);
534
+ generated.push(component);
535
+
536
+ // Add to registry
537
+ this.registry.components[type] = {
538
+ path: `components/extracted/${component.filename}`,
539
+ variants: component.variants,
540
+ instances: Object.values(variants).flat().length
541
+ };
542
+ }
543
+ }
544
+
545
+ return generated;
546
+ }
547
+
548
+ /**
549
+ * Write components to output directory
550
+ */
551
+ writeComponents(outputDir) {
552
+ if (!fs.existsSync(outputDir)) {
553
+ fs.mkdirSync(outputDir, { recursive: true });
554
+ }
555
+
556
+ const components = this.generateAll();
557
+ const written = [];
558
+
559
+ for (const component of components) {
560
+ const filePath = path.join(outputDir, component.filename);
561
+ fs.writeFileSync(filePath, component.content);
562
+ written.push(filePath);
563
+ }
564
+
565
+ // Write registry
566
+ const registryPath = path.join(outputDir, 'registry.json');
567
+ fs.writeFileSync(registryPath, JSON.stringify(this.registry, null, 2));
568
+ written.push(registryPath);
569
+
570
+ // Write index file
571
+ const indexContent = this.generateIndex(components);
572
+ const indexPath = path.join(outputDir, `index.${this.options.generateTypeScript ? 'ts' : 'js'}`);
573
+ fs.writeFileSync(indexPath, indexContent);
574
+ written.push(indexPath);
575
+
576
+ return written;
577
+ }
578
+
579
+ /**
580
+ * Generate index file
581
+ */
582
+ generateIndex(components) {
583
+ let content = '// Auto-generated component library index\n\n';
584
+
585
+ for (const component of components) {
586
+ const name = component.name;
587
+ content += `export { ${name} } from './${name}';\n`;
588
+ }
589
+
590
+ return content;
591
+ }
592
+
593
+ /**
594
+ * Get extraction summary
595
+ */
596
+ getSummary() {
597
+ const summary = {
598
+ totalComponents: this.components.size,
599
+ byType: {}
600
+ };
601
+
602
+ for (const [key, instances] of this.components.entries()) {
603
+ const type = key.split('-')[0];
604
+ if (!summary.byType[type]) {
605
+ summary.byType[type] = { variants: [], instances: 0 };
606
+ }
607
+ const variant = key.split('-').slice(1).join('-');
608
+ if (!summary.byType[type].variants.includes(variant)) {
609
+ summary.byType[type].variants.push(variant);
610
+ }
611
+ summary.byType[type].instances += instances.length;
612
+ }
613
+
614
+ return summary;
615
+ }
616
+
617
+ /**
618
+ * Format summary for CLI output
619
+ */
620
+ formatSummary() {
621
+ const summary = this.getSummary();
622
+ const lines = [];
623
+
624
+ lines.push('\x1b[1mComponent Library Extraction Summary\x1b[0m\n');
625
+ lines.push(`Total component types found: ${Object.keys(summary.byType).length}\n`);
626
+
627
+ for (const [type, data] of Object.entries(summary.byType)) {
628
+ lines.push(`\x1b[1m${type}\x1b[0m`);
629
+ lines.push(` Variants: ${data.variants.join(', ')}`);
630
+ lines.push(` Instances: ${data.instances}`);
631
+ lines.push('');
632
+ }
633
+
634
+ return lines.join('\n');
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Extract components from HTML directory
640
+ */
641
+ function extractComponents(htmlDir, options = {}) {
642
+ const extractor = new ComponentExtractor(options);
643
+ extractor.analyzeDirectory(htmlDir);
644
+ return extractor;
645
+ }
646
+
647
+ // CLI execution
648
+ if (require.main === module) {
649
+ const args = process.argv.slice(2);
650
+ let inputDir = null;
651
+ let outputDir = null;
652
+ let projectName = null;
653
+
654
+ for (let i = 0; i < args.length; i++) {
655
+ switch (args[i]) {
656
+ case '--input':
657
+ case '-i':
658
+ inputDir = args[++i];
659
+ break;
660
+ case '--output':
661
+ case '-o':
662
+ outputDir = args[++i];
663
+ break;
664
+ case '--project':
665
+ projectName = args[++i];
666
+ break;
667
+ case '--help':
668
+ case '-h':
669
+ console.log(`
670
+ Usage: node extract-components.js [options]
671
+
672
+ Options:
673
+ --input, -i <path> Input directory containing HTML files
674
+ --output, -o <path> Output directory for generated components
675
+ --project <name> Project name
676
+ --help, -h Show this help
677
+
678
+ Examples:
679
+ node extract-components.js --project my-app
680
+ node extract-components.js -i ./html -o ./components/extracted
681
+ `);
682
+ process.exit(0);
683
+ }
684
+ }
685
+
686
+ // Handle project-based paths
687
+ if (projectName) {
688
+ const SKILL_DIR = path.dirname(__dirname);
689
+ const PROJECTS_DIR = path.resolve(SKILL_DIR, '../../../projects');
690
+ const projectDir = path.join(PROJECTS_DIR, projectName);
691
+
692
+ inputDir = inputDir || path.join(projectDir, 'references', 'html');
693
+ outputDir = outputDir || path.join(projectDir, 'prototype', 'src', 'components', 'extracted');
694
+ }
695
+
696
+ if (!inputDir) {
697
+ console.error('\x1b[31mError:\x1b[0m --input or --project is required');
698
+ process.exit(1);
699
+ }
700
+
701
+ try {
702
+ console.log(`\n\x1b[1mComponent Library Extractor\x1b[0m`);
703
+ console.log(`Input: ${inputDir}`);
704
+
705
+ const extractor = extractComponents(inputDir);
706
+
707
+ console.log('');
708
+ console.log(extractor.formatSummary());
709
+
710
+ if (outputDir) {
711
+ console.log(`\x1b[1mWriting to:\x1b[0m ${outputDir}`);
712
+ const written = extractor.writeComponents(outputDir);
713
+ console.log(`\n\x1b[32m✓ Wrote ${written.length} files:\x1b[0m`);
714
+ for (const file of written) {
715
+ console.log(` ${path.basename(file)}`);
716
+ }
717
+ } else {
718
+ console.log('\x1b[33mTip:\x1b[0m Use --output <dir> to generate component files');
719
+ }
720
+
721
+ } catch (error) {
722
+ console.error(`\x1b[31mError:\x1b[0m ${error.message}`);
723
+ process.exit(1);
724
+ }
725
+ }
726
+
727
+ module.exports = {
728
+ ComponentExtractor,
729
+ extractComponents,
730
+ COMPONENT_PATTERNS
731
+ };