wds-add-object.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. // wds-add-object.js — WDS scaffold: append an object spec block to a page spec file
  2. // Usage: node src/scripts/wds-add-object.js --page "C-UX-Scenarios/01-onboarding/01-start/01-start.md" \
  3. // --section "Hero" --object "Primary Headline" --component "H1 heading" \
  4. // --se "Välkommen" --en "Welcome"
  5. 'use strict';
  6. const fs = require('node:fs');
  7. const path = require('node:path');
  8. function parseArgs(argv) {
  9. const args = {};
  10. for (let i = 0; i < argv.length; i++) {
  11. if (argv[i].startsWith('--')) {
  12. const key = argv[i].slice(2);
  13. const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : true;
  14. args[key] = value;
  15. if (value !== true) i++;
  16. }
  17. }
  18. return args;
  19. }
  20. function toSlug(str) {
  21. return str.toLowerCase().replaceAll(/\s+/g, '-');
  22. }
  23. function printUsage() {
  24. process.stdout.write(
  25. [
  26. 'Usage: node src/scripts/wds-add-object.js --page <path> --section <name> --object <name> [options]',
  27. '',
  28. 'Required:',
  29. ' --page Path to the page spec .md file',
  30. ' --section Section name (e.g. "Hero")',
  31. ' --object Object name (e.g. "Primary Headline")',
  32. '',
  33. 'Optional:',
  34. ' --component Component name (default: "—")',
  35. ' --translation Translation key (auto-derived if omitted)',
  36. ' --se Swedish text content',
  37. ' --en English text content',
  38. ' --behavior Behavior description (e.g. "onClick: submit form")',
  39. ' --component-path Path to component file (default: "—")',
  40. '',
  41. ].join('\n'),
  42. );
  43. }
  44. // Derive page slug from file path: "01-start/01-start.md" -> "01-start"
  45. function pageSlugFromPath(filePath) {
  46. const base = path.basename(filePath, '.md');
  47. return base;
  48. }
  49. // Derive Object ID: pageSlug + sectionSlug + objectSlug
  50. // e.g. page=01-start, section=Hero, object=Primary Headline -> 01-start-hero-primary-headline
  51. function deriveObjectId(pageSlug, sectionName, objectName) {
  52. // Strip leading page number from pageSlug for ID prefix
  53. // "01-start" -> "start", "02-signup" -> "signup"
  54. const slugParts = pageSlug.split('-');
  55. const pagePrefix = slugParts.length > 1 ? slugParts.slice(1).join('-') : pageSlug;
  56. const sectionSlug = toSlug(sectionName);
  57. const objectSlug = toSlug(objectName);
  58. return `${pagePrefix}-${sectionSlug}-${objectSlug}`;
  59. }
  60. function buildObjectBlock({ objectName, objectId, component, componentPath, translationKey, se, en, behavior }) {
  61. const compDisplay = componentPath && componentPath !== '—' ? `[${component}](${componentPath})` : component || '—';
  62. const lines = [
  63. `#### ${objectName}`,
  64. '',
  65. `**OBJECT ID:** \`${objectId}\``,
  66. '',
  67. '| Property | Value |',
  68. '|----------|-------|',
  69. `| Component | ${compDisplay} |`,
  70. `| Translation Key | \`${translationKey}\` |`,
  71. `| SE | "${se || '—'}" |`,
  72. `| EN | "${en || '—'}" |`,
  73. `| Behavior | ${behavior || '—'} |`,
  74. '',
  75. ];
  76. return lines.join('\n');
  77. }
  78. // Insert content after a section heading. Creates the section heading if it doesn't exist.
  79. function insertUnderSection(content, sectionHeading, objectBlock) {
  80. const lines = content.split('\n');
  81. const headingLine = `### Section: ${sectionHeading}`;
  82. const headingIdx = lines.findIndex((l) => l.trim() === headingLine);
  83. if (headingIdx === -1) {
  84. // Section doesn't exist — append it before the next ## heading after ## Page Sections
  85. const pageSectionsIdx = lines.findIndex((l) => l.trim() === '## Page Sections');
  86. if (pageSectionsIdx === -1) {
  87. // Just append at end before last nav row
  88. return content + `\n${headingLine}\n\n${objectBlock}\n`;
  89. }
  90. // Find end of ## Page Sections block
  91. let insertIdx = pageSectionsIdx + 1;
  92. while (insertIdx < lines.length) {
  93. const t = lines[insertIdx].trim();
  94. if (t.startsWith('## ') || t === '---') break;
  95. insertIdx++;
  96. }
  97. const before = lines.slice(0, insertIdx);
  98. const after = lines.slice(insertIdx);
  99. return [...before, '', headingLine, '', objectBlock, ...after].join('\n');
  100. } else {
  101. // Find the end of this section (next ### or ## or end of file)
  102. let insertIdx = headingIdx + 1;
  103. // Skip blank lines after heading
  104. while (insertIdx < lines.length && lines[insertIdx].trim() === '') insertIdx++;
  105. // Skip comment lines
  106. while (insertIdx < lines.length && lines[insertIdx].trim().startsWith('<!--')) insertIdx++;
  107. // Find end of section
  108. let endIdx = insertIdx;
  109. while (endIdx < lines.length) {
  110. const t = lines[endIdx].trim();
  111. if (t.startsWith('### ') || t.startsWith('## ') || t === '---') break;
  112. endIdx++;
  113. }
  114. // Insert object block before end of section
  115. const before = lines.slice(0, endIdx);
  116. const after = lines.slice(endIdx);
  117. return [...before, '', objectBlock, ...after].join('\n');
  118. }
  119. }
  120. function main() {
  121. const args = parseArgs(process.argv.slice(2));
  122. if (args.help) {
  123. printUsage();
  124. process.exit(0);
  125. }
  126. if (!args.page || !args.section || !args.object) {
  127. process.stderr.write('Error: --page, --section, and --object are required.\n\n');
  128. printUsage();
  129. process.exit(1);
  130. }
  131. const filePath = path.resolve(args.page);
  132. if (!fs.existsSync(filePath)) {
  133. process.stderr.write(`Error: File not found: ${filePath}\n`);
  134. process.exit(1);
  135. }
  136. const pageSlug = pageSlugFromPath(filePath);
  137. const objectId = deriveObjectId(pageSlug, args.section, args.object);
  138. // Auto-derive translation key from objectId
  139. const translationKey = args.translation || objectId.replaceAll('-', '.');
  140. const objectBlock = buildObjectBlock({
  141. objectName: args.object,
  142. objectId,
  143. component: args.component || '—',
  144. componentPath: args['component-path'] || '—',
  145. translationKey,
  146. se: args.se || '',
  147. en: args.en || '',
  148. behavior: args.behavior || '—',
  149. });
  150. let content;
  151. try {
  152. content = fs.readFileSync(filePath, 'utf8');
  153. } catch (error) {
  154. process.stderr.write(`Error reading file: ${error.message}\n`);
  155. process.exit(1);
  156. }
  157. // Check for duplicate object ID
  158. if (content.includes(`\`${objectId}\``)) {
  159. process.stderr.write(`Error: Object ID already exists in file: ${objectId}\n`);
  160. process.exit(1);
  161. }
  162. const updated = insertUnderSection(content, args.section, objectBlock);
  163. try {
  164. fs.writeFileSync(filePath, updated, 'utf8');
  165. } catch (error) {
  166. process.stderr.write(`Error writing file: ${error.message}\n`);
  167. process.exit(1);
  168. }
  169. process.stdout.write(`✓ Added object ${objectId}\n`);
  170. process.stdout.write(` File: ${filePath}\n`);
  171. }
  172. main();