update-workshop-site-navigation.mjs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import crypto from 'node:crypto';
  4. import assert from 'node:assert/strict';
  5. // Change navigation only; keep the current multi-round manual and report bodies.
  6. const root = 'docs/workshop-quality-r13-20261110';
  7. const out = 'docs/workshop-dialogue-guide-20261111/site-navigation';
  8. const manual = 'final-康师傅工作坊操作手册.html';
  9. const sha = value => crypto.createHash('sha256').update(value).digest('hex');
  10. const read = file => fs.readFileSync(path.join(root, file), 'utf8');
  11. const write = (file, text) => fs.writeFileSync(path.join(root, file), text);
  12. const walk = dir => fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
  13. const file = path.join(dir, entry.name);
  14. return entry.isDirectory() ? walk(file) : [file];
  15. });
  16. fs.mkdirSync(out, { recursive: true });
  17. const articleHashes = {};
  18. const reportPages = [];
  19. for (const file of walk(root).filter(file => file.endsWith('.html'))) {
  20. const rel = path.relative(root, file).replaceAll('\\', '/');
  21. let html = read(rel);
  22. if (!html.includes('<header class="site-topbar">') || rel === 'index.html') continue;
  23. const articles = [...html.matchAll(/<article\b[\s\S]*?<\/article>/g)].map(match => match[0]);
  24. articles.forEach((article, index) => { articleHashes[`${rel}#${index}`] = sha(article); });
  25. const base = rel.includes('/') ? '../' : '';
  26. const header = `<header class="site-topbar"><span>产品创新工作坊</span><nav class="site-links" aria-label="网站导航"><a href="${base}${manual}">操作手册</a><a href="${base}案例报告合集.html" aria-current="page">报告案例</a><a href="https://kangshifu.fmode.cn/" target="_blank" rel="noopener">成果看板 <span aria-hidden="true">↗</span></a></nav></header>`;
  27. assert.equal([...html.matchAll(/<header class="site-topbar">/g)].length, 1, rel);
  28. html = html.replace(/<header class="site-topbar">[\s\S]*?<\/header>/, () => header)
  29. .replaceAll(`href="${base}index.html"`, `href="${base}${manual}"`)
  30. .replaceAll('>工作坊首页</a>', '>操作手册</a>');
  31. assert.deepEqual([...html.matchAll(/<article\b[\s\S]*?<\/article>/g)].map(match => match[0]), articles);
  32. write(rel, html);
  33. reportPages.push(rel);
  34. }
  35. let css = read('assets/workshop-reports.css');
  36. css = css.replace('justify-content:space-between;gap:18px;align-items:center;padding:14px 28px;z-index:20',
  37. 'gap:18px;align-items:center;height:51px;padding:0 28px;z-index:20')
  38. .replace('.site-links{display:flex;align-items:center;gap:20px}', '.site-links{display:flex;align-items:center;justify-content:flex-end;margin-left:auto;gap:20px}')
  39. .replace('.site-topbar{padding:14px 22px}', '.site-topbar{padding:0 22px}')
  40. .replace('html{scroll-padding-top:24px}.site-layout{display:block}', 'html{scroll-padding-top:70px}.site-layout{display:block;padding-top:51px}')
  41. .replace('.site-topbar{position:static;padding:11px 16px}', '.site-topbar{position:fixed;top:0;left:0;right:0;height:51px;padding:0 16px}')
  42. .replace('.site-links{width:100%;gap:18px}.site-links a:last-child{margin-left:auto}', '.site-links{gap:18px}')
  43. .replace('.source-item{scroll-margin-top:16px}', '.source-item{scroll-margin-top:70px}');
  44. write('assets/workshop-reports.css', css);
  45. let html = read(manual);
  46. html = html.replace('.topbar>span{margin-right:auto;font-size:11px}.topbar>a{font-size:11px}', '.topbar>span{display:none}.top-links{gap:18px}')
  47. .replace('.topbar>span{font-size:10px}.topbar>a{font-size:10px}', '.top-links{gap:16px}');
  48. write(manual, html);
  49. function redirect(target) {
  50. return `<!doctype html>
  51. <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
  52. <title>康师傅 · 产品创新工作坊操作手册</title>
  53. <script>
  54. const destination = new URL(${JSON.stringify(target)}, location.href);
  55. destination.search = location.search;
  56. destination.hash = location.hash;
  57. location.replace(destination.href);
  58. </script>
  59. <noscript><meta http-equiv="refresh" content="0;url=${target}"></noscript>
  60. </head><body><p><a href="${target}">打开康师傅工作坊操作手册</a></p></body></html>
  61. `;
  62. }
  63. write('index.html', redirect(manual));
  64. write('manual/index.html', redirect('../' + manual));
  65. const routes = JSON.parse(read('site-routes.json'));
  66. routes.home = manual;
  67. routes.manual = manual;
  68. routes.redirects = { 'index.html': manual, 'manual/index.html': manual };
  69. routes.navigation = [
  70. { label: '操作手册', path: manual },
  71. { label: '报告案例', path: '案例报告合集.html' },
  72. { label: '成果看板', url: 'https://kangshifu.fmode.cn/' }
  73. ];
  74. write('site-routes.json', JSON.stringify(routes, null, 2) + '\n');
  75. const dataHashes = Object.fromEntries(walk(path.join(root, 'data')).map(file => [path.relative(root, file).replaceAll('\\', '/'), sha(fs.readFileSync(file))]));
  76. const beforeFile = path.join(out, 'before.json');
  77. if (!fs.existsSync(beforeFile)) fs.writeFileSync(beforeFile, JSON.stringify({ reportPages, articleHashes, dataHashes }, null, 2) + '\n');
  78. console.log(JSON.stringify({ ok: true, reportPagesUpdated: reportPages.length, home: manual, navigationLinks: 3 }));