main.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. const { walk, EXCLUDE_LISTS } = require('../file-walker/test');
  2. const fs = require('fs').promises;
  3. const path_ = require('node:path');
  4. const FILE_EXCLUDES = [
  5. /(^|\/)\.git/,
  6. /^volatile\//,
  7. /^node_modules\//,
  8. /\/node_modules$/,
  9. /^submodules\//,
  10. /^node_modules$/,
  11. /package-lock\.json/,
  12. /^src\/dev-center\/js/,
  13. /src\/backend\/src\/public\/assets/,
  14. /^src\/gui\/src\/lib/,
  15. /^eslint\.config\.js$/,
  16. // translation readme copies
  17. /(^|\/)doc\/i18n/,
  18. // irrelevant documentation
  19. /(^|\/)doc\/graveyard/,
  20. // development logs
  21. /\/devlog\.md$/,
  22. ]
  23. const ROOT_DIR = path_.join(__dirname, '../..');
  24. const WIKI_DIR = path_.join(__dirname, '../../submodules/wiki');
  25. const path_to_name = path => {
  26. // Special case for Home.md
  27. if ( path === 'doc/README.md' ) return 'Home';
  28. // Remove src/ and doc/ components
  29. // path = path.replace(/src\//g, '')
  30. path = path.replace(/doc\//g, '')
  31. // Hyphenate components
  32. path = path.replace(/-/g, '_')
  33. path = path.replace(/\//g, '-')
  34. // Remove extension
  35. path = path.replace(/\.md$/, '')
  36. return path;
  37. }
  38. const fix_relative_links = (content, entry) => {
  39. const originalDir = path_.dirname(entry);
  40. // Markdown links: [text](path/to/file.md), [text](path/to/file#section), etc
  41. return content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, link) => {
  42. // Skip external links
  43. if (link.startsWith('http://') || link.startsWith('https://') || link.startsWith('/')) {
  44. return match;
  45. }
  46. // Anchor links within the same file aren't changed
  47. if (link.startsWith('#')) return match;
  48. // Split the link to separate the path from the anchor
  49. const [linkPath, anchor] = link.split('#');
  50. // Resolve the relative path
  51. let resolvedPath = path_.normalize(path_.join(originalDir, linkPath));
  52. // Find the matching wiki path
  53. const wikiPath = path_to_name(resolvedPath);
  54. const newLink = anchor ? `${wikiPath}#${anchor}` : wikiPath;
  55. return `[${text}](${newLink})`;
  56. });
  57. };
  58. const main = async () => {
  59. const walk_iter = walk({
  60. excludes: FILE_EXCLUDES,
  61. }, ROOT_DIR);
  62. const documents = [];
  63. for await ( const value of walk_iter ) {
  64. let path = value.path;
  65. path = path_.relative(ROOT_DIR, path);
  66. // File must be under a doc/ directory
  67. if ( ! path.match(/(^|\/)doc\//) ) continue;
  68. // File must be markdown
  69. if ( ! path.match(/\.md/) ) continue;
  70. let outputName = path_to_name(path);
  71. // Read file content
  72. let content = await fs.readFile(value.path, 'utf8');
  73. // Get the first heading from the file to use as title
  74. const titleMatch = content.match(/^#\s+(.+)$/m);
  75. const title = titleMatch ? titleMatch[1] : outputName.replace(/-/g, ' ');
  76. // Fix internal links
  77. content = fix_relative_links(content, path);
  78. // Write the modified content to the wiki directory
  79. await fs.writeFile(path_.join(WIKI_DIR, outputName + '.md'), content);
  80. // Store information for sidebar
  81. const sidebarPath = outputName.split('-');
  82. // The original path structure (minus doc/) helps determine the hierarchy
  83. documents.push({
  84. sidebarPath,
  85. outputName,
  86. title: title
  87. });
  88. }
  89. // Generate _Sidebar.md
  90. const sidebarContent = generate_sidebar(documents);
  91. await fs.writeFile(path_.join(WIKI_DIR, '_Sidebar.md'), sidebarContent);
  92. }
  93. const format_name = name => {
  94. if ( name === 'api' ) return 'API';
  95. if ( name === 'contributors' ) return 'For Contributors';
  96. return name.charAt(0).toUpperCase() + name.slice(1);
  97. }
  98. const generate_sidebar = (documents) => {
  99. // Sort entries by path to group related files together
  100. documents.sort((a, b) => {
  101. const pathA = a.sidebarPath.slice(0, -1).join('/');
  102. const pathB = b.sidebarPath.slice(0, -1).join('/');
  103. if ( pathA !== pathB ) {
  104. return pathA.localeCompare(pathB);
  105. }
  106. // README.md always goes first
  107. const isReadmeA = a.outputName.toLowerCase().includes('readme') ||
  108. a.outputName.toLowerCase().includes('home');
  109. const isReadmeB = b.outputName.toLowerCase().includes('readme') ||
  110. b.outputName.toLowerCase().includes('home');
  111. if (isReadmeA) return -1;
  112. if (isReadmeB) return 1;
  113. return a.title.localeCompare(b.title);
  114. });
  115. // Format a document link the same way everywhere
  116. const formatDocumentLink = (document) => {
  117. let title = document.title;
  118. if ( document.outputName.split('-').slice(-1)[0].toLowerCase() === 'readme' ) {
  119. title = 'Index (README.md)';
  120. }
  121. if ( document.outputName.split('-').slice(-1)[0].toLowerCase() === 'home' ) {
  122. title = `Home`;
  123. }
  124. return `* [${title}](${document.outputName.replace('.md', '')})\n`;
  125. };
  126. // Recursive function to build sidebar sections
  127. const buildSection = (docs, depth = 0, prefix = '') => {
  128. let result = '';
  129. const directDocs = [];
  130. const subSections = new Map();
  131. // Separate direct documents from those in subsections
  132. for (const doc of docs) {
  133. if (doc.sidebarPath.length <= depth + 1) {
  134. // Direct document at this level
  135. directDocs.push(doc);
  136. } else {
  137. // Document belongs in a subsection
  138. const sectionName = doc.sidebarPath[depth];
  139. if (!subSections.has(sectionName)) {
  140. subSections.set(sectionName, []);
  141. }
  142. subSections.get(sectionName).push(doc);
  143. }
  144. }
  145. // Add direct documents
  146. for (const doc of directDocs) {
  147. result += formatDocumentLink(doc);
  148. }
  149. // Process subsections recursively
  150. for (const [sectionName, sectionDocs] of subSections.entries()) {
  151. // Generate heading with appropriate level
  152. const headingLevel = '#'.repeat(depth + 2);
  153. const formattedName = format_name(sectionName)
  154. result += `\n${headingLevel} ${formattedName}\n`;
  155. // Process the subsection documents
  156. result += buildSection(sectionDocs, depth + 1, `${prefix}${sectionName}/`);
  157. }
  158. return result;
  159. };
  160. // Start with the main heading
  161. let sidebar = "## General\n\n";
  162. // Split documents into top-level and those in sections
  163. const topLevelDocs = documents.filter(doc => doc.sidebarPath.length <= 1);
  164. const sectionDocs = documents.filter(doc => doc.sidebarPath.length > 1);
  165. // Add top-level documents
  166. for (const doc of topLevelDocs) {
  167. sidebar += formatDocumentLink(doc);
  168. }
  169. // Group the remaining documents by their top-level sections
  170. const topLevelSections = new Map();
  171. for (const doc of sectionDocs) {
  172. const sectionName = doc.sidebarPath[0];
  173. if (!topLevelSections.has(sectionName)) {
  174. topLevelSections.set(sectionName, []);
  175. }
  176. topLevelSections.get(sectionName).push(doc);
  177. }
  178. // Process each top-level section
  179. for (const [sectionName, sectionDocs] of topLevelSections.entries()) {
  180. const formattedName = format_name(sectionName);
  181. sidebar += `\n## ${formattedName}\n`;
  182. sidebar += buildSection(sectionDocs, 1, `${sectionName}/`);
  183. }
  184. return sidebar;
  185. };
  186. main();