@newest-ai/notes-pipeline 0.0.0-stage → 0.1.0

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.
Files changed (135) hide show
  1. package/README.md +111 -2
  2. package/bin/cli.mjs +89 -0
  3. package/package.json +47 -4
  4. package/src/build.mjs +147 -0
  5. package/src/config.mjs +101 -0
  6. package/src/images.mjs +77 -0
  7. package/src/markdown.mjs +255 -0
  8. package/src/page.mjs +48 -0
  9. package/src/scan.mjs +196 -0
  10. package/src/server.mjs +64 -0
  11. package/src/sync.mjs +45 -0
  12. package/ui-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  13. package/ui-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  14. package/ui-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  15. package/ui-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  16. package/ui-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  17. package/ui-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  18. package/ui-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  19. package/ui-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  20. package/ui-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  21. package/ui-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  22. package/ui-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  23. package/ui-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  24. package/ui-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  25. package/ui-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  26. package/ui-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  27. package/ui-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  28. package/ui-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  29. package/ui-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  30. package/ui-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  31. package/ui-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  32. package/ui-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  33. package/ui-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  34. package/ui-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  35. package/ui-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  36. package/ui-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  37. package/ui-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  38. package/ui-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  39. package/ui-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  40. package/ui-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  41. package/ui-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  42. package/ui-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  43. package/ui-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  44. package/ui-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  45. package/ui-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  46. package/ui-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  47. package/ui-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  48. package/ui-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  49. package/ui-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  50. package/ui-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  51. package/ui-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  52. package/ui-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  53. package/ui-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  54. package/ui-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  55. package/ui-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  56. package/ui-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  57. package/ui-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  58. package/ui-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  59. package/ui-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  60. package/ui-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  61. package/ui-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  62. package/ui-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  63. package/ui-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  64. package/ui-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  65. package/ui-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  66. package/ui-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  67. package/ui-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  68. package/ui-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  69. package/ui-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  70. package/ui-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  71. package/ui-dist/assets/abnfDiagram-VCTEODGH-CDHx7YLL.js +1 -0
  72. package/ui-dist/assets/arc-DjGAUyp_.js +1 -0
  73. package/ui-dist/assets/architectureDiagram-5GKGNRK7-B6tP0R76.js +36 -0
  74. package/ui-dist/assets/blockDiagram-I7D4REHJ-CJKj3a7w.js +129 -0
  75. package/ui-dist/assets/c4Diagram-7LVT6UL2-CczZ7G4M.js +38 -0
  76. package/ui-dist/assets/channel-BSjIpAHK.js +1 -0
  77. package/ui-dist/assets/chunk-2Q5K7J3B-DzpdqUGz.js +1 -0
  78. package/ui-dist/assets/chunk-5VM5RSS4-BeJ6VQDZ.js +15 -0
  79. package/ui-dist/assets/chunk-F27PBJKO-DpwWrj6p.js +1 -0
  80. package/ui-dist/assets/chunk-IMKFNOWR--IGBlGaQ.js +231 -0
  81. package/ui-dist/assets/chunk-JWPE2WC7-DjfryBzD.js +1 -0
  82. package/ui-dist/assets/chunk-POPQ4Y6H-DffxLFFj.js +1 -0
  83. package/ui-dist/assets/chunk-SVP7TREG-CE6_V2jm.js +88 -0
  84. package/ui-dist/assets/chunk-TICWLB2K-Bmo2jE18.js +206 -0
  85. package/ui-dist/assets/chunk-XXDRQBXY-DgFJ5FID.js +1 -0
  86. package/ui-dist/assets/classDiagram-ZZMXUADV-r_Zsn-w2.js +1 -0
  87. package/ui-dist/assets/classDiagram-v2-VYDZK3BY-r_Zsn-w2.js +1 -0
  88. package/ui-dist/assets/cose-bilkent-JH36ORCC-EpYvKhkR.js +1 -0
  89. package/ui-dist/assets/cynefin-OW5HDTMX-m874sF5c.js +178 -0
  90. package/ui-dist/assets/cynefinDiagram-5FMLGOSQ-BEHVBTtx.js +62 -0
  91. package/ui-dist/assets/cytoscape.esm-BLAcgzS2.js +331 -0
  92. package/ui-dist/assets/dagre-GXQ25YYZ-B5xpr7mU.js +4 -0
  93. package/ui-dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  94. package/ui-dist/assets/diagram-S7CK7UJ4-By-KXXKZ.js +30 -0
  95. package/ui-dist/assets/diagram-UQ7AKVKN-98Z_zLBW.js +41 -0
  96. package/ui-dist/assets/diagram-VSXAHHWV-DorTwt19.js +3 -0
  97. package/ui-dist/assets/diagram-VX7I27RA-CcLVdZdI.js +24 -0
  98. package/ui-dist/assets/diagram-Z3DM3KII-CoU-15RW.js +24 -0
  99. package/ui-dist/assets/ebnfDiagram-PWID7BFC-LOS933k1.js +1 -0
  100. package/ui-dist/assets/erDiagram-RLTQ6QDP-DCOhkWMm.js +99 -0
  101. package/ui-dist/assets/flowDiagram-HODETNUW-DCELuOay.js +168 -0
  102. package/ui-dist/assets/ganttDiagram-EL5Y4UJY-DN86wUza.js +292 -0
  103. package/ui-dist/assets/gitGraphDiagram-WWUBYQGX-DtAeX_qF.js +106 -0
  104. package/ui-dist/assets/index-0dcAv40L.js +18 -0
  105. package/ui-dist/assets/index-B2aZBtOC.css +10 -0
  106. package/ui-dist/assets/infoDiagram-27XIBGKW-DNm-kcG3.js +2 -0
  107. package/ui-dist/assets/init-Gi6I4Gst.js +1 -0
  108. package/ui-dist/assets/ishikawaDiagram-5VMMS53U-CiiC7W_C.js +70 -0
  109. package/ui-dist/assets/journeyDiagram-3NMN7TZE-CPAdWfBY.js +139 -0
  110. package/ui-dist/assets/kanban-definition-UXKFOSKX-BhVQC1_-.js +89 -0
  111. package/ui-dist/assets/katex-C5jXJg4s.js +257 -0
  112. package/ui-dist/assets/layout-BlXzpK8o.js +1 -0
  113. package/ui-dist/assets/linear-8shfR-80.js +1 -0
  114. package/ui-dist/assets/mermaid.core-BCnYUvY2.js +312 -0
  115. package/ui-dist/assets/mindmap-definition-YA3MSWOX-Cpw1GfG6.js +96 -0
  116. package/ui-dist/assets/ordinal-Cboi1Yqb.js +1 -0
  117. package/ui-dist/assets/pegDiagram-XKGWAZYB-BYdKPqTy.js +1 -0
  118. package/ui-dist/assets/pieDiagram-E7YTZNPT-TkX0tmEb.js +39 -0
  119. package/ui-dist/assets/quadrantDiagram-AXDQQJYC-Dh4eVW8R.js +7 -0
  120. package/ui-dist/assets/railroadDiagram-O6MQD6OU-DTlzAG0n.js +1 -0
  121. package/ui-dist/assets/requirementDiagram-BXWQKSXE-oUofCYZ-.js +84 -0
  122. package/ui-dist/assets/sankeyDiagram-P5KCCOFB-DeR69bDu.js +40 -0
  123. package/ui-dist/assets/sequenceDiagram-WJ2MYXX4-UUD3vSQW.js +162 -0
  124. package/ui-dist/assets/sizeCapture-INFHLROL-NFwVkWux.js +1 -0
  125. package/ui-dist/assets/stateDiagram-D77RDMKH-D1ZuZVUL.js +1 -0
  126. package/ui-dist/assets/stateDiagram-v2-MP3YSRHH-mAQKG0Lc.js +1 -0
  127. package/ui-dist/assets/swimlanes-42K2YHIH-C7Z6kG6j.js +1 -0
  128. package/ui-dist/assets/swimlanesDiagram-VR7AAH4N-DwZoKb6v.js +8 -0
  129. package/ui-dist/assets/timeline-definition-24CTP7MA-AG6fl_Ff.js +120 -0
  130. package/ui-dist/assets/vennDiagram-4TSXK5OY-Csc1K4_t.js +34 -0
  131. package/ui-dist/assets/wardleyDiagram-VM6X3IG4-DP8m660g.js +78 -0
  132. package/ui-dist/assets/xychartDiagram-S5SC5T6Z-CKBO5hAQ.js +7 -0
  133. package/ui-dist/favicon.svg +10 -0
  134. package/ui-dist/gongan.png +0 -0
  135. package/ui-dist/index.html +15 -0
package/README.md CHANGED
@@ -1,3 +1,112 @@
1
- # Temporary Holding Version
1
+ # @newest-ai/notes-pipeline
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 把笔记仓库编译成静态笔记站点:**Markdown → 预构建 Vue SPA**。
4
+ 主站、子站共用同一套界面与编译逻辑,站点差异只体现在 `blog.config.mjs`。
5
+
6
+ ## 安装
7
+
8
+ ```bash
9
+ npm i -D @newest-ai/notes-pipeline
10
+ ```
11
+
12
+ ## 命令
13
+
14
+ ```bash
15
+ npx notes-pipeline build # 同步(如配置了 source)+ 编译到 dist/
16
+ npx notes-pipeline sync # 只同步笔记源到 workspace/
17
+ npx notes-pipeline dev # 编译后启动本地预览
18
+ ```
19
+
20
+ 选项:
21
+
22
+ | 参数 | 说明 |
23
+ |---|---|
24
+ | `--config <file>` | 配置文件,默认 `blog.config.mjs` |
25
+ | `--base <path>` | 站点部署路径,覆盖 `site.base`,如 `/agent/` |
26
+ | `--source <mode>` | 强制笔记源模式:`local` / `git` |
27
+ | `--port <number>` | dev 端口,默认 `devPort`(4321) |
28
+
29
+ ## 笔记目录约定
30
+
31
+ - 顶层目录 = 分类,子目录任意深度;`NN-` 编号前缀只影响排序,展示标题会自动去掉。
32
+ - 根目录散落的 `.md` 归入「概览」(`content.rootSkip` 中的文件除外)。
33
+ - 文章标题取文件名;`[[wikilink]]`、`[文本](相对.md)` 自动解析为站内文章。
34
+ - 图片支持相对路径、`./images/…`、`![[image.png]]`,按 basename 全局匹配(大小写不敏感)。
35
+
36
+ ## 配置
37
+
38
+ ```js
39
+ export default {
40
+ site: {
41
+ title: '智能体笔记',
42
+ brand: '智能体笔记', // 顶部品牌文字
43
+ brandAccent: '', // brand 尾部高亮片段(如 'AI')
44
+ description: '…',
45
+ lang: 'zh-CN',
46
+ base: '/agent/', // 部署路径,必须以 / 开头、/ 结尾
47
+ url: 'https://newest-ai.com/agent/',
48
+ heroText: '个人网站 · 无商业目的 · 不接广告 · 合规守法',
49
+ footer: {
50
+ siteName: '智能体笔记', // 可选
51
+ domain: 'newest-ai.com', // 可选
52
+ icp: { text: '…', href: '…' },
53
+ gongan: { text: '…', href: '…' },
54
+ bio: '作者简介…', // 可选
55
+ mainSite: 'https://newest-ai.com', // 子站可选:回链主站
56
+ text: '个人笔记 · 无商业目的 · 不接广告 · 合规守法',
57
+ },
58
+ },
59
+
60
+ content: {
61
+ dir: 'notes', // 笔记目录(未配置 source 时生效)
62
+ exclude: ['.obsidian', '.git', 'node_modules'],
63
+ rootSkip: ['readme.md', 'idea.md'],
64
+ rootSectionTitle: '概览',
65
+ },
66
+
67
+ // 笔记源可选:笔记在别的仓库时先同步到 workspace/,再编译
68
+ source: {
69
+ mode: 'git', // 'git' | 'local'
70
+ dir: 'D:/Notes/ai-notes', // local 模式源目录
71
+ workspace: 'workspace',
72
+ git: { url: 'git@…', branch: 'master', depth: 1 },
73
+ },
74
+
75
+ // 菜单分级:main = 目录之上再抽一层类别;sub = 顶层目录直接当菜单
76
+ menu: {
77
+ type: 'main',
78
+ groups: [
79
+ { title: '基本功', dirs: ['01-语言基础', '02-操作系统'] },
80
+ ],
81
+ },
82
+
83
+ output: 'dist',
84
+ devPort: 4321,
85
+ }
86
+ ```
87
+
88
+ ### 菜单类型
89
+
90
+ | 类型 | 说明 | 树结构 |
91
+ |---|---|---|
92
+ | `main` | 主站:在目录之上抽一层类别 | `group → category → subcategory → article` |
93
+ | `sub` | 子站:顶层目录直接作为菜单 | `category → subcategory → article` |
94
+
95
+ `menu.groups` 中未出现的目录会平铺在类别之后;「概览」始终置顶。
96
+
97
+ ## 产物
98
+
99
+ ```
100
+ dist/
101
+ ├── index.html # 注入站点配置与页面标题
102
+ ├── assets/ # 预构建 UI(js/css/字体)
103
+ ├── favicon.svg
104
+ ├── gongan.png
105
+ └── content/
106
+ ├── tree.json # 导航树 + 扁平索引
107
+ ├── site.json # 站点配置快照
108
+ ├── articles/<id>.json # 每篇文章 HTML、标题、大纲、上/下一篇
109
+ └── images/ # 笔记引用的图片
110
+ ```
111
+
112
+ 部署时把 `dist/` 托管到 `site.base` 对应的路径即可。资源与数据请求均使用以 `base` 开头的绝对路径。
package/bin/cli.mjs ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * notes-pipeline CLI
4
+ *
5
+ * notes-pipeline build [--config blog.config.mjs] [--base /agent/] [--source local|git]
6
+ * notes-pipeline sync [--config blog.config.mjs] [--source local|git]
7
+ * notes-pipeline dev [--port 4321] [...同 build]
8
+ */
9
+ import path from 'node:path'
10
+ import { buildSite } from '../src/build.mjs'
11
+ import { loadConfig } from '../src/config.mjs'
12
+ import { syncSource } from '../src/sync.mjs'
13
+ import { startServer } from '../src/server.mjs'
14
+
15
+ const HELP = `
16
+ notes-pipeline — 笔记仓库编译为静态站点
17
+
18
+ 用法:
19
+ notes-pipeline build [选项] 同步(如配置了 source)+ 编译到 dist/
20
+ notes-pipeline sync [选项] 只同步笔记源到 workspace/
21
+ notes-pipeline dev [选项] 编译后启动本地静态服务器
22
+
23
+ 选项:
24
+ --config <file> 配置文件,默认 blog.config.mjs
25
+ --base <path> 站点部署路径,覆盖配置里的 site.base,如 /agent/
26
+ --source <mode> 强制笔记源模式:local | git
27
+ --port <number> dev 端口,默认取配置 devPort(4321)
28
+ -h, --help 显示帮助
29
+ `.trim()
30
+
31
+ const COMMANDS = new Set(['build', 'sync', 'dev'])
32
+
33
+ function parseArgs(argv) {
34
+ const opts = {}
35
+ let command = 'build'
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const arg = argv[i]
38
+ if (arg === '-h' || arg === '--help') {
39
+ opts.help = true
40
+ } else if (arg === '--config') {
41
+ opts.configPath = argv[++i]
42
+ } else if (arg === '--base') {
43
+ opts.base = argv[++i]
44
+ } else if (arg === '--source') {
45
+ opts.sourceMode = argv[++i]
46
+ } else if (arg === '--port') {
47
+ opts.port = Number(argv[++i])
48
+ } else if (COMMANDS.has(arg)) {
49
+ command = arg
50
+ } else {
51
+ throw new Error(`未知参数:${arg}`)
52
+ }
53
+ }
54
+ return { command, opts }
55
+ }
56
+
57
+ async function main() {
58
+ const { command, opts } = parseArgs(process.argv.slice(2))
59
+ if (opts.help) {
60
+ console.log(HELP)
61
+ return
62
+ }
63
+
64
+ const root = process.cwd()
65
+
66
+ if (command === 'sync') {
67
+ const config = await loadConfig(root, { configPath: opts.configPath, sourceMode: opts.sourceMode })
68
+ if (!config.source) {
69
+ console.log('ℹ️ 当前配置未设置 source,笔记直接在 content.dir 中,无需同步。')
70
+ return
71
+ }
72
+ const result = syncSource(config)
73
+ console.log(`✅ 同步完成(${result.mode})→ ${path.relative(root, config.contentDir)}`)
74
+ return
75
+ }
76
+
77
+ if (command === 'dev') {
78
+ const { config, outDir } = await buildSite({ ...opts, root })
79
+ startServer({ dir: outDir, port: opts.port || config.devPort })
80
+ return
81
+ }
82
+
83
+ await buildSite({ ...opts, root })
84
+ }
85
+
86
+ main().catch((err) => {
87
+ console.error(`❌ ${err?.message || err}`)
88
+ process.exit(1)
89
+ })
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@newest-ai/notes-pipeline",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "笔记仓库 → 静态笔记站点的编译流水线(Markdown → Vue SPA),支持主站分组菜单与子站平铺菜单",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "notes-pipeline": "./bin/cli.mjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "ui-dist",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "scripts": {
20
+ "build:ui": "vite build",
21
+ "pack:check": "npm run build:ui && npm pack --dry-run",
22
+ "prepack": "npm run build:ui"
23
+ },
24
+ "keywords": [
25
+ "notes",
26
+ "static-site",
27
+ "markdown",
28
+ "vue",
29
+ "vite"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://gitee.com/code-beast/notes-pipeline.git"
34
+ },
35
+ "dependencies": {
36
+ "highlight.js": "^11.11.1",
37
+ "katex": "^0.16.21",
38
+ "markdown-it": "^14.1.0",
39
+ "markdown-it-anchor": "^9.2.0",
40
+ "markdown-it-task-lists": "^2.1.1",
41
+ "markdown-it-texmath": "^1.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@vitejs/plugin-vue": "^5.2.1",
45
+ "mermaid": "^11.4.1",
46
+ "vite": "^6.0.7",
47
+ "vue": "^3.5.13"
48
+ }
49
+ }
package/src/build.mjs ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * 编译入口:笔记目录 → 静态站点产物。
3
+ *
4
+ * 流程:同步(可选)→ 清理产物 → 拷贝预构建 UI → 扫描笔记 → 渲染 Markdown
5
+ * → 写文章 JSON / 导航树 / 站点配置 → 生成 index.html
6
+ */
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ import { loadConfig } from './config.mjs'
12
+ import { syncSource } from './sync.mjs'
13
+ import { scanNotes, flattenRoots, applyMenuGroups } from './scan.mjs'
14
+ import { createImages } from './images.mjs'
15
+ import { createMarkdown, extractHeadings } from './markdown.mjs'
16
+ import { buildSitePayload, renderIndexHtml } from './page.mjs'
17
+
18
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
19
+ const UI_DIST = path.join(PKG_ROOT, 'ui-dist')
20
+
21
+ const readText = (abs) =>
22
+ fs.readFileSync(abs, 'utf8').replace(/^\uFEFF/, '').replace(/\r\n/g, '\n')
23
+
24
+ function ensureUiBuilt() {
25
+ if (!fs.existsSync(path.join(UI_DIST, 'index.html'))) {
26
+ throw new Error(`UI 产物缺失:${UI_DIST}(请在包内执行 npm run build:ui)`)
27
+ }
28
+ }
29
+
30
+ function copyUi(outDir) {
31
+ for (const e of fs.readdirSync(UI_DIST, { withFileTypes: true })) {
32
+ if (e.name === 'index.html') continue
33
+ fs.cpSync(path.join(UI_DIST, e.name), path.join(outDir, e.name), { recursive: true })
34
+ }
35
+ }
36
+
37
+ function writeFile(outDir, rel, content) {
38
+ const dest = path.join(outDir, rel)
39
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
40
+ fs.writeFileSync(dest, content, 'utf8')
41
+ }
42
+
43
+ /**
44
+ * @param {{ root?: string, configPath?: string, base?: string, sourceMode?: 'local'|'git',
45
+ * skipSync?: boolean, log?: (msg: string) => void }} options
46
+ */
47
+ export async function buildSite(options = {}) {
48
+ const root = options.root || process.cwd()
49
+ const log = options.log || console.log
50
+
51
+ log('\n🔨 编译笔记 → 静态站点\n')
52
+
53
+ const config = await loadConfig(root, {
54
+ configPath: options.configPath || 'blog.config.mjs',
55
+ base: options.base,
56
+ sourceMode: options.sourceMode,
57
+ })
58
+
59
+ if (config.source && !options.skipSync) {
60
+ const r = syncSource(config)
61
+ log(` 已同步(${r.mode})→ ${path.relative(root, config.contentDir)}`)
62
+ }
63
+
64
+ const contentDir = config.contentDir
65
+ if (!fs.existsSync(contentDir)) throw new Error(`笔记目录不存在:${contentDir}`)
66
+
67
+ ensureUiBuilt()
68
+
69
+ const outDir = path.resolve(root, config.output)
70
+ fs.rmSync(outDir, { recursive: true, force: true })
71
+ fs.mkdirSync(outDir, { recursive: true })
72
+ copyUi(outDir)
73
+
74
+ const scan = scanNotes(contentDir, config.content)
75
+ const roots = config.menu.type === 'main' ? applyMenuGroups(scan.roots, config.menu.groups) : scan.roots
76
+ const flat = flattenRoots(roots)
77
+
78
+ log(` 扫描:Markdown ${scan.mdFiles.length} 篇(收录 ${flat.length} 篇),图片 ${scan.imageFiles.length} 张`)
79
+
80
+ // 文章查找表(wikilink / .md 链接解析用)
81
+ const byId = new Map()
82
+ const byBasename = new Map()
83
+ for (const a of flat) {
84
+ byId.set(a.id.toLowerCase(), a.id)
85
+ const name = path.posix.basename(a.id).toLowerCase()
86
+ if (!byBasename.has(name)) byBasename.set(name, a.id)
87
+ }
88
+
89
+ const images = createImages({ imageFiles: scan.imageFiles, outDir })
90
+ const md = createMarkdown({ images, articles: { byId, byBasename }, base: config.site.base })
91
+
92
+ const nodeTitle = new Map()
93
+ const collectTitle = (node) => {
94
+ if (node.type === 'article') nodeTitle.set(node.id, node.title)
95
+ else (node.children || []).forEach(collectTitle)
96
+ }
97
+ roots.forEach(collectTitle)
98
+
99
+ const mdByRel = new Map(scan.mdFiles.map((f) => [f.rel, f.abs]))
100
+ const rendered = new Map()
101
+
102
+ flat.forEach((item, i) => {
103
+ const rel = `${item.id}.md`
104
+ const abs = mdByRel.get(rel)
105
+ if (!abs) return
106
+ const source = readText(abs)
107
+ const html = md.render(source, { dir: path.posix.dirname(rel) })
108
+ const prev = i > 0 ? flat[i - 1] : null
109
+ const next = i < flat.length - 1 ? flat[i + 1] : null
110
+ const payload = {
111
+ id: item.id,
112
+ title: nodeTitle.get(item.id) || item.title,
113
+ html,
114
+ headings: extractHeadings(html),
115
+ prev: prev ? { id: prev.id, title: prev.title } : null,
116
+ next: next ? { id: next.id, title: next.title } : null,
117
+ source: rel,
118
+ }
119
+ rendered.set(item.id, payload)
120
+ writeFile(outDir, item.file, JSON.stringify(payload))
121
+ })
122
+
123
+ const tree = {
124
+ meta: {
125
+ generatedAt: new Date().toISOString(),
126
+ total: flat.length,
127
+ menuType: config.menu.type,
128
+ source: config.source
129
+ ? (config.source.mode === 'git' ? config.source.git.url : config.source.dir)
130
+ : config.content.dir,
131
+ },
132
+ roots,
133
+ flat,
134
+ }
135
+ writeFile(outDir, 'content/tree.json', JSON.stringify(tree))
136
+
137
+ const sitePayload = buildSitePayload({ config, total: flat.length })
138
+ writeFile(outDir, 'content/site.json', JSON.stringify(sitePayload))
139
+
140
+ const template = fs.readFileSync(path.join(UI_DIST, 'index.html'), 'utf8')
141
+ writeFile(outDir, 'index.html', renderIndexHtml(template, { payload: sitePayload, base: config.site.base }))
142
+
143
+ log(` 渲染:文章 ${rendered.size} 篇,引用图片 ${images.copied.size} 张`)
144
+ log(`✅ 完成:${path.relative(root, outDir) || config.output}/\n`)
145
+
146
+ return { config, outDir, total: flat.length, images: images.copied.size }
147
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * 站点配置加载与规范化。
3
+ *
4
+ * 配置文件(默认 blog.config.mjs)默认导出一个对象,字段见 README。
5
+ * 这里补齐默认值并把 base 规范为以 / 开头、以 / 结尾的形式。
6
+ */
7
+ import path from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+
10
+ const DEFAULT_EXCLUDE = ['.git', '.obsidian', '.idea', '.hermes', 'node_modules']
11
+
12
+ export function normalizeBase(base) {
13
+ let b = String(base || '/').trim()
14
+ if (!b.startsWith('/')) b = `/${b}`
15
+ if (!b.endsWith('/')) b += '/'
16
+ return b.replace(/\/{2,}/g, '/')
17
+ }
18
+
19
+ function normalizeSource(source, root) {
20
+ const mode = source.mode === 'local' ? 'local' : 'git'
21
+ const workspace = source.workspace || 'workspace'
22
+ const dir = source.dir ? path.resolve(root, source.dir) : null
23
+ if (mode === 'local' && !dir) throw new Error('source.mode=local 时必须提供 source.dir')
24
+ if (mode === 'git' && !(source.git && source.git.url)) throw new Error('source.mode=git 时必须提供 source.git.url')
25
+ return {
26
+ mode,
27
+ dir,
28
+ workspace,
29
+ git: {
30
+ url: source.git?.url || '',
31
+ branch: source.git?.branch || 'master',
32
+ depth: source.git?.depth ?? 1,
33
+ },
34
+ }
35
+ }
36
+
37
+ function normalizeMenu(menu) {
38
+ if (!menu) return { type: 'sub', groups: [] }
39
+ const type = menu.type === 'main' ? 'main' : 'sub'
40
+ const groups = Array.isArray(menu.groups) ? menu.groups : []
41
+ if (type === 'main' && groups.length === 0) {
42
+ throw new Error('menu.type=main 时必须提供 menu.groups(顶层类别 → 目录名列表)')
43
+ }
44
+ for (const g of groups) {
45
+ if (!g.title || !Array.isArray(g.dirs)) throw new Error('menu.groups 每项必须是 { title, dirs: [] }')
46
+ }
47
+ return { type, groups }
48
+ }
49
+
50
+ /**
51
+ * @param {string} root 站点项目根目录
52
+ * @param {{ configPath?: string, base?: string, sourceMode?: string }} options
53
+ */
54
+ export async function loadConfig(root, options = {}) {
55
+ const abs = path.resolve(root, options.configPath || 'blog.config.mjs')
56
+ let raw
57
+ try {
58
+ raw = (await import(pathToFileURL(abs).href)).default
59
+ } catch (e) {
60
+ throw new Error(`无法加载配置文件 ${abs}:${e.message}`)
61
+ }
62
+ if (!raw || typeof raw !== 'object') throw new Error(`配置文件 ${abs} 必须默认导出对象`)
63
+
64
+ const site = { ...(raw.site || {}) }
65
+ site.base = normalizeBase(options.base || site.base)
66
+ site.title = site.title || 'Notes'
67
+ site.brand = site.brand || site.title
68
+ site.brandAccent = site.brandAccent || ''
69
+ site.description = site.description || ''
70
+ site.lang = site.lang || 'zh-CN'
71
+ site.url = site.url || ''
72
+ site.heroText = site.heroText || ''
73
+ site.footer = { ...(site.footer || {}) }
74
+
75
+ const content = {
76
+ dir: raw.content?.dir || 'notes',
77
+ exclude: raw.content?.exclude || DEFAULT_EXCLUDE,
78
+ rootSkip: raw.content?.rootSkip ?? ['readme.md', 'idea.md'],
79
+ rootSectionTitle: raw.content?.rootSectionTitle || '概览',
80
+ }
81
+
82
+ let source = raw.source ? normalizeSource(raw.source, root) : null
83
+ if (source && options.sourceMode) {
84
+ const mode = options.sourceMode === 'local' ? 'local' : 'git'
85
+ source = normalizeSource({ ...source, mode }, root)
86
+ }
87
+
88
+ const menu = normalizeMenu(raw.menu)
89
+
90
+ return {
91
+ root,
92
+ site,
93
+ content,
94
+ source,
95
+ menu,
96
+ output: raw.output || 'dist',
97
+ devPort: raw.devPort || 4321,
98
+ // 有 source 时正文来自 workspace;否则来自 content.dir
99
+ contentDir: path.resolve(root, source ? source.workspace : content.dir),
100
+ }
101
+ }
package/src/images.mjs ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 图片解析与拷贝。
3
+ *
4
+ * 解析顺序:相对当前 md 所在目录 → 去掉 ./ 再试 → 按 basename 全局匹配(大小写不敏感)。
5
+ * 输出名做 ASCII 净化并去重,写入 dist/content/images/。
6
+ */
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+
10
+ export function sanitizeName(base) {
11
+ return base.replace(/\s+/g, '_').replace(/[^\w.-]+/g, '_')
12
+ }
13
+
14
+ export function createImages({ imageFiles, outDir }) {
15
+ const byPath = new Map() // 小写 rel -> rel
16
+ const byBasename = new Map() // 小写 basename -> rel
17
+ for (const img of imageFiles) {
18
+ const rel = img.rel
19
+ byPath.set(rel.toLowerCase(), rel)
20
+ const key = img.base.toLowerCase()
21
+ if (!byBasename.has(key)) byBasename.set(key, rel)
22
+ }
23
+
24
+ const copied = new Map() // rel -> 输出文件名
25
+ const usedNames = new Set()
26
+
27
+ function resolve(ref, fromDir = '') {
28
+ let clean = String(ref || '').trim()
29
+ // 去掉 Obsidian 图片后缀: #pic_center、|623、#right 等
30
+ clean = clean.replace(/[#|].*$/, '').trim()
31
+ if (!clean) return null
32
+ // 绝对 URL / data:
33
+ if (/^[a-z][a-z0-9+.-]*:/i.test(clean)) return null
34
+
35
+ let rel = path.posix.normalize(path.posix.join(fromDir || '.', clean))
36
+ if (byPath.has(rel.toLowerCase())) return byPath.get(rel.toLowerCase())
37
+
38
+ if (clean.startsWith('./')) {
39
+ rel = path.posix.normalize(path.posix.join(fromDir || '.', clean.slice(2)))
40
+ if (byPath.has(rel.toLowerCase())) return byPath.get(rel.toLowerCase())
41
+ }
42
+
43
+ const base = path.posix.basename(clean).toLowerCase()
44
+ if (byBasename.has(base)) return byBasename.get(base)
45
+ return null
46
+ }
47
+
48
+ function copy(rel) {
49
+ if (!rel) return null
50
+ if (copied.has(rel)) return copied.get(rel)
51
+ const src = imageFiles.find((i) => i.rel === rel)
52
+ if (!src) return null
53
+
54
+ const base = sanitizeName(path.posix.basename(rel))
55
+ let finalName = base
56
+ let n = 1
57
+ while (usedNames.has(finalName)) {
58
+ const ext = path.extname(base)
59
+ finalName = `${path.basename(base, ext)}_${n++}${ext}`
60
+ }
61
+ usedNames.add(finalName)
62
+
63
+ const dest = path.join(outDir, 'content', 'images', finalName)
64
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
65
+ fs.copyFileSync(src.abs, dest)
66
+ copied.set(rel, finalName)
67
+ return finalName
68
+ }
69
+
70
+ return {
71
+ resolve,
72
+ copy,
73
+ get copied() {
74
+ return copied
75
+ },
76
+ }
77
+ }