wds-nav.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. // wds-nav.js — WDS scaffold: generate/update navigation links across all pages in a scenario
  2. // Usage: node src/scripts/wds-nav.js --scenario "01 Onboarding"
  3. // node src/scripts/wds-nav.js --all
  4. 'use strict';
  5. const fs = require('node:fs');
  6. const path = require('node:path');
  7. function parseArgs(argv) {
  8. const args = {};
  9. for (let i = 0; i < argv.length; i++) {
  10. if (argv[i].startsWith('--')) {
  11. const key = argv[i].slice(2);
  12. const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : true;
  13. args[key] = value;
  14. if (value !== true) i++;
  15. }
  16. }
  17. return args;
  18. }
  19. function toSlug(str) {
  20. return str.toLowerCase().replaceAll(/\s+/g, '-');
  21. }
  22. function printUsage() {
  23. process.stdout.write(
  24. [
  25. 'Usage: node src/scripts/wds-nav.js --scenario "01 Onboarding"',
  26. ' node src/scripts/wds-nav.js --all',
  27. '',
  28. 'Options:',
  29. ' --scenario Scenario name or slug to update',
  30. ' --all Update all scenarios',
  31. ' --output Base path (default: current directory)',
  32. '',
  33. ].join('\n'),
  34. );
  35. }
  36. // Build a human-readable page name from the slug for nav labels
  37. // e.g. "01-start" -> "01 Start"
  38. function slugToLabel(slug) {
  39. return slug
  40. .split('-')
  41. .map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
  42. .join(' ');
  43. }
  44. function buildNavRow(prev, next) {
  45. const leftPart = prev ? `← [${slugToLabel(prev.slug)}](../${prev.slug}/${prev.slug}.md)` : '←';
  46. const rightPart = next ? `[${slugToLabel(next.slug)} →](../${next.slug}/${next.slug}.md)` : '→';
  47. return `${leftPart} | ${rightPart}`;
  48. }
  49. // Replace all 3 occurrences of navigation rows in a page file.
  50. // Nav rows are lines matching the pattern: starts with '←' or contains '| [' and ends with '→'
  51. // We identify them by a simple pattern: line that starts with "←" (after trim).
  52. function updateNavInContent(content, navRow) {
  53. const lines = content.split('\n');
  54. const result = [];
  55. let navCount = 0;
  56. for (const line of lines) {
  57. const trimmed = line.trim();
  58. // Match navigation rows: lines that start with "←" (our nav format)
  59. if (trimmed.startsWith('←')) {
  60. result.push(navRow);
  61. navCount++;
  62. } else {
  63. result.push(line);
  64. }
  65. }
  66. return { content: result.join('\n'), navCount };
  67. }
  68. function getPageFolders(scenarioDir) {
  69. let entries;
  70. try {
  71. entries = fs.readdirSync(scenarioDir, { withFileTypes: true });
  72. } catch {
  73. return [];
  74. }
  75. return entries
  76. .filter((e) => e.isDirectory())
  77. .map((e) => e.name)
  78. .filter((name) => {
  79. // Must have a matching .md file inside
  80. const mdFile = path.join(scenarioDir, name, `${name}.md`);
  81. return fs.existsSync(mdFile);
  82. })
  83. .sort(); // Sort alphabetically — page numbers ensure correct order
  84. }
  85. function processScenario(scenariosBase, scenarioSlug) {
  86. const scenarioDir = path.join(scenariosBase, scenarioSlug);
  87. if (!fs.existsSync(scenarioDir)) {
  88. process.stderr.write(`Error: Scenario not found: ${scenarioDir}\n`);
  89. return false;
  90. }
  91. const pageSlugs = getPageFolders(scenarioDir);
  92. if (pageSlugs.length === 0) {
  93. process.stdout.write(` ${scenarioSlug}: no pages found, skipping.\n`);
  94. return true;
  95. }
  96. let updated = 0;
  97. for (let i = 0; i < pageSlugs.length; i++) {
  98. const slug = pageSlugs[i];
  99. const prev = i > 0 ? { slug: pageSlugs[i - 1] } : null;
  100. const next = i < pageSlugs.length - 1 ? { slug: pageSlugs[i + 1] } : null;
  101. const navRow = buildNavRow(prev, next);
  102. const mdFile = path.join(scenarioDir, slug, `${slug}.md`);
  103. let content;
  104. try {
  105. content = fs.readFileSync(mdFile, 'utf8');
  106. } catch (error) {
  107. process.stderr.write(` Error reading ${mdFile}: ${error.message}\n`);
  108. continue;
  109. }
  110. const { content: newContent, navCount } = updateNavInContent(content, navRow);
  111. if (navCount === 0) {
  112. process.stderr.write(` Warning: No navigation rows found in ${slug}.md\n`);
  113. }
  114. try {
  115. fs.writeFileSync(mdFile, newContent, 'utf8');
  116. updated++;
  117. } catch (error) {
  118. process.stderr.write(` Error writing ${mdFile}: ${error.message}\n`);
  119. }
  120. }
  121. process.stdout.write(`✓ Updated navigation for ${updated} pages in ${scenarioSlug}\n`);
  122. return true;
  123. }
  124. function main() {
  125. const args = parseArgs(process.argv.slice(2));
  126. if (args.help) {
  127. printUsage();
  128. process.exit(0);
  129. }
  130. if (!args.scenario && !args.all) {
  131. process.stderr.write('Error: --scenario or --all is required.\n\n');
  132. printUsage();
  133. process.exit(1);
  134. }
  135. const outputBase = args.output || process.cwd();
  136. const scenariosBase = path.join(outputBase, 'C-UX-Scenarios');
  137. if (!fs.existsSync(scenariosBase)) {
  138. process.stderr.write(`Error: C-UX-Scenarios directory not found at: ${scenariosBase}\n`);
  139. process.exit(1);
  140. }
  141. if (args.all) {
  142. let entries;
  143. try {
  144. entries = fs.readdirSync(scenariosBase, { withFileTypes: true });
  145. } catch (error) {
  146. process.stderr.write(`Error reading C-UX-Scenarios: ${error.message}\n`);
  147. process.exit(1);
  148. }
  149. const scenarios = entries
  150. .filter((e) => e.isDirectory())
  151. .map((e) => e.name)
  152. .sort();
  153. if (scenarios.length === 0) {
  154. process.stdout.write('No scenarios found.\n');
  155. process.exit(0);
  156. }
  157. let allOk = true;
  158. for (const slug of scenarios) {
  159. if (!processScenario(scenariosBase, slug)) allOk = false;
  160. }
  161. process.exit(allOk ? 0 : 1);
  162. } else {
  163. const scenarioSlug = toSlug(args.scenario);
  164. const ok = processScenario(scenariosBase, scenarioSlug);
  165. process.exit(ok ? 0 : 1);
  166. }
  167. }
  168. main();