@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.
- package/README.md +111 -2
- package/bin/cli.mjs +89 -0
- package/package.json +47 -4
- package/src/build.mjs +147 -0
- package/src/config.mjs +101 -0
- package/src/images.mjs +77 -0
- package/src/markdown.mjs +255 -0
- package/src/page.mjs +48 -0
- package/src/scan.mjs +196 -0
- package/src/server.mjs +64 -0
- package/src/sync.mjs +45 -0
- package/ui-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/ui-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/ui-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/ui-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/ui-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/ui-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/ui-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/ui-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/ui-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/ui-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/ui-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/ui-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/ui-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/ui-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/ui-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/ui-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/ui-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/ui-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/ui-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/ui-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/ui-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/ui-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/ui-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/ui-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/ui-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/ui-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/ui-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/ui-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/ui-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/ui-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/ui-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/ui-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/ui-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/ui-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/ui-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/ui-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/ui-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/ui-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/ui-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/ui-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/ui-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/ui-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/ui-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/ui-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/ui-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/ui-dist/assets/abnfDiagram-VCTEODGH-CDHx7YLL.js +1 -0
- package/ui-dist/assets/arc-DjGAUyp_.js +1 -0
- package/ui-dist/assets/architectureDiagram-5GKGNRK7-B6tP0R76.js +36 -0
- package/ui-dist/assets/blockDiagram-I7D4REHJ-CJKj3a7w.js +129 -0
- package/ui-dist/assets/c4Diagram-7LVT6UL2-CczZ7G4M.js +38 -0
- package/ui-dist/assets/channel-BSjIpAHK.js +1 -0
- package/ui-dist/assets/chunk-2Q5K7J3B-DzpdqUGz.js +1 -0
- package/ui-dist/assets/chunk-5VM5RSS4-BeJ6VQDZ.js +15 -0
- package/ui-dist/assets/chunk-F27PBJKO-DpwWrj6p.js +1 -0
- package/ui-dist/assets/chunk-IMKFNOWR--IGBlGaQ.js +231 -0
- package/ui-dist/assets/chunk-JWPE2WC7-DjfryBzD.js +1 -0
- package/ui-dist/assets/chunk-POPQ4Y6H-DffxLFFj.js +1 -0
- package/ui-dist/assets/chunk-SVP7TREG-CE6_V2jm.js +88 -0
- package/ui-dist/assets/chunk-TICWLB2K-Bmo2jE18.js +206 -0
- package/ui-dist/assets/chunk-XXDRQBXY-DgFJ5FID.js +1 -0
- package/ui-dist/assets/classDiagram-ZZMXUADV-r_Zsn-w2.js +1 -0
- package/ui-dist/assets/classDiagram-v2-VYDZK3BY-r_Zsn-w2.js +1 -0
- package/ui-dist/assets/cose-bilkent-JH36ORCC-EpYvKhkR.js +1 -0
- package/ui-dist/assets/cynefin-OW5HDTMX-m874sF5c.js +178 -0
- package/ui-dist/assets/cynefinDiagram-5FMLGOSQ-BEHVBTtx.js +62 -0
- package/ui-dist/assets/cytoscape.esm-BLAcgzS2.js +331 -0
- package/ui-dist/assets/dagre-GXQ25YYZ-B5xpr7mU.js +4 -0
- package/ui-dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/ui-dist/assets/diagram-S7CK7UJ4-By-KXXKZ.js +30 -0
- package/ui-dist/assets/diagram-UQ7AKVKN-98Z_zLBW.js +41 -0
- package/ui-dist/assets/diagram-VSXAHHWV-DorTwt19.js +3 -0
- package/ui-dist/assets/diagram-VX7I27RA-CcLVdZdI.js +24 -0
- package/ui-dist/assets/diagram-Z3DM3KII-CoU-15RW.js +24 -0
- package/ui-dist/assets/ebnfDiagram-PWID7BFC-LOS933k1.js +1 -0
- package/ui-dist/assets/erDiagram-RLTQ6QDP-DCOhkWMm.js +99 -0
- package/ui-dist/assets/flowDiagram-HODETNUW-DCELuOay.js +168 -0
- package/ui-dist/assets/ganttDiagram-EL5Y4UJY-DN86wUza.js +292 -0
- package/ui-dist/assets/gitGraphDiagram-WWUBYQGX-DtAeX_qF.js +106 -0
- package/ui-dist/assets/index-0dcAv40L.js +18 -0
- package/ui-dist/assets/index-B2aZBtOC.css +10 -0
- package/ui-dist/assets/infoDiagram-27XIBGKW-DNm-kcG3.js +2 -0
- package/ui-dist/assets/init-Gi6I4Gst.js +1 -0
- package/ui-dist/assets/ishikawaDiagram-5VMMS53U-CiiC7W_C.js +70 -0
- package/ui-dist/assets/journeyDiagram-3NMN7TZE-CPAdWfBY.js +139 -0
- package/ui-dist/assets/kanban-definition-UXKFOSKX-BhVQC1_-.js +89 -0
- package/ui-dist/assets/katex-C5jXJg4s.js +257 -0
- package/ui-dist/assets/layout-BlXzpK8o.js +1 -0
- package/ui-dist/assets/linear-8shfR-80.js +1 -0
- package/ui-dist/assets/mermaid.core-BCnYUvY2.js +312 -0
- package/ui-dist/assets/mindmap-definition-YA3MSWOX-Cpw1GfG6.js +96 -0
- package/ui-dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/ui-dist/assets/pegDiagram-XKGWAZYB-BYdKPqTy.js +1 -0
- package/ui-dist/assets/pieDiagram-E7YTZNPT-TkX0tmEb.js +39 -0
- package/ui-dist/assets/quadrantDiagram-AXDQQJYC-Dh4eVW8R.js +7 -0
- package/ui-dist/assets/railroadDiagram-O6MQD6OU-DTlzAG0n.js +1 -0
- package/ui-dist/assets/requirementDiagram-BXWQKSXE-oUofCYZ-.js +84 -0
- package/ui-dist/assets/sankeyDiagram-P5KCCOFB-DeR69bDu.js +40 -0
- package/ui-dist/assets/sequenceDiagram-WJ2MYXX4-UUD3vSQW.js +162 -0
- package/ui-dist/assets/sizeCapture-INFHLROL-NFwVkWux.js +1 -0
- package/ui-dist/assets/stateDiagram-D77RDMKH-D1ZuZVUL.js +1 -0
- package/ui-dist/assets/stateDiagram-v2-MP3YSRHH-mAQKG0Lc.js +1 -0
- package/ui-dist/assets/swimlanes-42K2YHIH-C7Z6kG6j.js +1 -0
- package/ui-dist/assets/swimlanesDiagram-VR7AAH4N-DwZoKb6v.js +8 -0
- package/ui-dist/assets/timeline-definition-24CTP7MA-AG6fl_Ff.js +120 -0
- package/ui-dist/assets/vennDiagram-4TSXK5OY-Csc1K4_t.js +34 -0
- package/ui-dist/assets/wardleyDiagram-VM6X3IG4-DP8m660g.js +78 -0
- package/ui-dist/assets/xychartDiagram-S5SC5T6Z-CKBO5hAQ.js +7 -0
- package/ui-dist/favicon.svg +10 -0
- package/ui-dist/gongan.png +0 -0
- package/ui-dist/index.html +15 -0
package/README.md
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @newest-ai/notes-pipeline
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|