@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
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Markdown 渲染管线:代码高亮、KaTeX、Mermaid、wikilink、站内链接与图片重写。
3
+ *
4
+ * createMarkdown({ images, articles, base }) 返回 markdown-it 实例:
5
+ * images createImages() 产物,负责图片解析与拷贝
6
+ * articles { byId, byBasename } 文章查找表
7
+ * base 站点 base(如 '/'、'/agent/'),用于产出绝对图片地址
8
+ */
9
+ import path from 'node:path'
10
+ import MarkdownIt from 'markdown-it'
11
+ import anchor from 'markdown-it-anchor'
12
+ import taskLists from 'markdown-it-task-lists'
13
+ import texmath from 'markdown-it-texmath'
14
+ import katex from 'katex'
15
+ import hljs from 'highlight.js/lib/common'
16
+ import twig from 'highlight.js/lib/languages/twig'
17
+ import powershell from 'highlight.js/lib/languages/powershell'
18
+
19
+ hljs.registerLanguage('twig', twig)
20
+ hljs.registerLanguage('powershell', powershell)
21
+
22
+ const LANG_ALIAS = {
23
+ shell: 'bash',
24
+ bash: 'bash',
25
+ sh: 'bash',
26
+ console: 'plaintext',
27
+ terminal: 'plaintext',
28
+ text: 'plaintext',
29
+ plaintext: 'plaintext',
30
+ url: 'plaintext',
31
+ ts: 'typescript',
32
+ typescript: 'typescript',
33
+ powshell: 'powershell',
34
+ powershell: 'powershell',
35
+ jinja: 'twig',
36
+ }
37
+
38
+ function headingSlug(text) {
39
+ let t = String(text)
40
+ .replace(/`([^`]*)`/g, '$1')
41
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
42
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
43
+ .replace(/[*_~]{1,2}([^*_~]+)[*_~]{1,2}/g, '$1')
44
+ .replace(/<[^>]+>/g, '')
45
+ .trim()
46
+ t = t.replace(/[^\p{L}\p{N}\s\-_]/gu, '')
47
+ t = t.replace(/\s+/g, '-')
48
+ return t || 'section'
49
+ }
50
+
51
+ const stripTags = (html) =>
52
+ html
53
+ .replace(/<[^>]*>/g, '')
54
+ .replace(/&amp;/g, '&')
55
+ .replace(/&lt;/g, '<')
56
+ .replace(/&gt;/g, '>')
57
+ .replace(/&quot;/g, '"')
58
+ .trim()
59
+
60
+ export function extractHeadings(html) {
61
+ const headings = []
62
+ const re = /<h([1-6])\b[^>]*\bid="([^"]*)"[^>]*>([\s\S]*?)<\/h\1>/gi
63
+ let m
64
+ while ((m = re.exec(html))) {
65
+ headings.push({ level: +m[1], id: m[2], text: stripTags(m[3]) })
66
+ }
67
+ return headings
68
+ }
69
+
70
+ export function createMarkdown({ images, articles, base = '/' }) {
71
+ const imageBase = `${base}content/images/`
72
+
73
+ function resolveArticle(target) {
74
+ let t = String(target || '').trim().replace(/\.md$/i, '')
75
+ t = t.replace(/\\/g, '/')
76
+ const key = t.toLowerCase()
77
+ if (articles.byBasename.has(key)) return articles.byBasename.get(key)
78
+ const norm = path.posix.normalize(key)
79
+ if (articles.byId.has(norm)) return articles.byId.get(norm)
80
+ return null
81
+ }
82
+
83
+ function resolveMdLink(href, dir) {
84
+ let pathPart = href
85
+ const hashIdx = href.indexOf('#')
86
+ if (hashIdx >= 0) pathPart = href.slice(0, hashIdx)
87
+ pathPart = pathPart.trim()
88
+ if (!pathPart) return null
89
+ if (/^[a-z][a-z0-9+.-]*:/i.test(pathPart)) return null
90
+ if (/\.(png|jpe?g|gif|svg|webp|pdf|docx?|xlsx?|zip|bmp)$/i.test(pathPart)) return null
91
+ const rel = path.posix.normalize(path.posix.join(dir || '.', pathPart)).replace(/\.md$/i, '')
92
+ const key = rel.toLowerCase()
93
+ if (articles.byId.has(key)) return articles.byId.get(key)
94
+ const basename = path.posix.basename(rel).toLowerCase()
95
+ if (articles.byBasename.has(basename)) return articles.byBasename.get(basename)
96
+ return null
97
+ }
98
+
99
+ const md = new MarkdownIt({ html: true, linkify: true, breaks: false })
100
+
101
+ md.use(anchor, { slugify: headingSlug, level: [1, 2, 3, 4, 5, 6] })
102
+ md.use(taskLists, { enabled: true, label: true })
103
+ md.use(texmath, {
104
+ engine: katex,
105
+ delimiters: ['dollars', 'brackets', 'beg_end'],
106
+ katexOptions: { throwOnError: false, strict: false, errorColor: '#cc0000', trust: true },
107
+ })
108
+
109
+ function highlight(code, lang) {
110
+ const l = (LANG_ALIAS[lang] || lang || 'plaintext').toLowerCase()
111
+ if (l === 'plaintext') return md.utils.escapeHtml(code)
112
+ if (hljs.getLanguage(l)) {
113
+ try {
114
+ return hljs.highlight(code, { language: l }).value
115
+ } catch {
116
+ return md.utils.escapeHtml(code)
117
+ }
118
+ }
119
+ return md.utils.escapeHtml(code)
120
+ }
121
+
122
+ md.renderer.rules.fence = (tokens, idx) => {
123
+ const token = tokens[idx]
124
+ const info = (token.info || '').trim()
125
+ const lang = info.split(/\s+/)[0].toLowerCase()
126
+ const code = token.content
127
+ if (lang === 'mermaid' || lang === 'mermaid.js') {
128
+ return `<pre class="mermaid">${md.utils.escapeHtml(code)}</pre>\n`
129
+ }
130
+ const highlighted = highlight(code, lang)
131
+ const cls = `hljs language-${md.utils.escapeHtml(lang || 'plaintext')}`
132
+ return `<pre class="code-block"><code class="${cls}">${highlighted}</code></pre>\n`
133
+ }
134
+
135
+ // wikilink:[[目标]] / [[目标|别名]] / [[目标#锚点]]
136
+ function wikilinkRule(state, silent) {
137
+ const src = state.src
138
+ const start = state.pos
139
+ if (src.charCodeAt(start) !== 0x5b || src.charCodeAt(start + 1) !== 0x5b) return false
140
+ const end = src.indexOf(']]', start + 2)
141
+ if (end < 0) return false
142
+ const inner = src.slice(start + 2, end)
143
+ if (inner.includes('\n') || inner.includes('[')) return false
144
+
145
+ if (!silent) {
146
+ let target = inner
147
+ let alias = null
148
+ let anchorText = ''
149
+ const hashIdx = target.indexOf('#')
150
+ if (hashIdx >= 0) {
151
+ anchorText = target.slice(hashIdx + 1)
152
+ target = target.slice(0, hashIdx)
153
+ }
154
+ const pipeIdx = target.indexOf('|')
155
+ if (pipeIdx >= 0) {
156
+ alias = target.slice(pipeIdx + 1)
157
+ target = target.slice(0, pipeIdx)
158
+ }
159
+ target = target.trim()
160
+ const text = alias || target || inner
161
+
162
+ const artId = resolveArticle(target)
163
+ if (artId) {
164
+ const open = state.push('link_open', 'a', 1)
165
+ open.attrs = [['href', `#/article/${encodeURIComponent(artId)}`]]
166
+ if (anchorText) open.attrs.push(['data-anchor', anchorText])
167
+ const t = state.push('text', '', 0)
168
+ t.content = text
169
+ state.push('link_close', 'a', -1)
170
+ } else {
171
+ const imgRel = images.resolve(target, '')
172
+ if (imgRel) {
173
+ const outName = images.copy(imgRel)
174
+ const open = state.push('link_open', 'a', 1)
175
+ open.attrs = [['href', imageBase + outName], ['target', '_blank']]
176
+ const t = state.push('text', '', 0)
177
+ t.content = text
178
+ state.push('link_close', 'a', -1)
179
+ } else {
180
+ const t = state.push('text', '', 0)
181
+ t.content = text
182
+ }
183
+ }
184
+ }
185
+ state.pos = end + 2
186
+ return true
187
+ }
188
+
189
+ // 图片嵌入:![[image.png]]
190
+ function wikilinkEmbedRule(state, silent) {
191
+ const src = state.src
192
+ const start = state.pos
193
+ if (src.charCodeAt(start) !== 0x21) return false
194
+ if (src.charCodeAt(start + 1) !== 0x5b || src.charCodeAt(start + 2) !== 0x5b) return false
195
+ const end = src.indexOf(']]', start + 3)
196
+ if (end < 0) return false
197
+ const inner = src.slice(start + 3, end)
198
+ if (inner.includes('\n')) return false
199
+ const imgRel = images.resolve(inner, '')
200
+ if (!imgRel) return false
201
+ if (!silent) {
202
+ const tok = state.push('image', 'img', 0)
203
+ tok.attrs = [['src', `img::${imgRel}`], ['alt', inner]]
204
+ tok.content = inner
205
+ }
206
+ state.pos = end + 2
207
+ return true
208
+ }
209
+
210
+ md.inline.ruler.before('link', 'wikilink', wikilinkRule)
211
+ md.inline.ruler.before('image', 'wikilink_embed', wikilinkEmbedRule)
212
+
213
+ md.renderer.rules.image = (tokens, idx, options, env, self) => {
214
+ const token = tokens[idx]
215
+ if (token.children && token.children.length) {
216
+ token.attrSet('alt', self.renderInlineAsText(token.children, options, env))
217
+ } else if (token.content) {
218
+ token.attrSet('alt', token.content)
219
+ }
220
+ const src = token.attrGet('src') || ''
221
+ let outName = null
222
+ if (src.startsWith('img::')) {
223
+ outName = images.copy(src.slice(5))
224
+ } else {
225
+ const rel = images.resolve(src, env.dir || '')
226
+ if (rel) outName = images.copy(rel)
227
+ }
228
+ if (outName) {
229
+ token.attrSet('src', imageBase + outName)
230
+ } else {
231
+ token.attrSet('data-broken', 'true')
232
+ }
233
+ token.attrSet('loading', 'lazy')
234
+ return self.renderToken(tokens, idx, options)
235
+ }
236
+
237
+ md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
238
+ const token = tokens[idx]
239
+ const href = token.attrGet('href') || ''
240
+ if (href.startsWith('#/article/')) return self.renderToken(tokens, idx, options)
241
+ if (href.startsWith('#')) return self.renderToken(tokens, idx, options)
242
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href)) {
243
+ if (/^https?:/i.test(href)) {
244
+ token.attrSet('target', '_blank')
245
+ token.attrSet('rel', 'noopener noreferrer')
246
+ }
247
+ return self.renderToken(tokens, idx, options)
248
+ }
249
+ const artId = resolveMdLink(href, env.dir || '')
250
+ if (artId) token.attrSet('href', `#/article/${encodeURIComponent(artId)}`)
251
+ return self.renderToken(tokens, idx, options)
252
+ }
253
+
254
+ return md
255
+ }
package/src/page.mjs ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 页面装配:生成站点运行时配置,并把预构建 UI 的 index.html 改写为站点入口。
3
+ */
4
+ const esc = (s) =>
5
+ String(s ?? '')
6
+ .replace(/&/g, '&amp;')
7
+ .replace(/</g, '&lt;')
8
+ .replace(/>/g, '&gt;')
9
+ .replace(/"/g, '&quot;')
10
+
11
+ /** 注入到 index.html、供前端读取的站点配置 */
12
+ export function buildSitePayload({ config, total }) {
13
+ return {
14
+ title: config.site.title,
15
+ brand: config.site.brand,
16
+ brandAccent: config.site.brandAccent,
17
+ description: config.site.description,
18
+ lang: config.site.lang,
19
+ base: config.site.base,
20
+ heroText: config.site.heroText,
21
+ menuType: config.menu.type,
22
+ total,
23
+ footer: config.site.footer,
24
+ generatedAt: new Date().toISOString(),
25
+ }
26
+ }
27
+
28
+ /**
29
+ * @param {string} template ui-dist/index.html 内容
30
+ * @param {{ payload: object, base: string }} options
31
+ */
32
+ export function renderIndexHtml(template, { payload, base }) {
33
+ const inline = JSON.stringify(payload).replace(/</g, '\\u003c')
34
+ return template
35
+ .replace(/<html lang="[^"]*"/, `<html lang="${esc(payload.lang)}"`)
36
+ .replace(/<title>[\s\S]*?<\/title>/, `<title>${esc(payload.title)}</title>`)
37
+ .replace(
38
+ /<meta name="description" content="[^"]*"\s*\/?>/,
39
+ `<meta name="description" content="${esc(payload.description)}">`,
40
+ )
41
+ .replace(/\.\/assets\//g, `${base}assets/`)
42
+ .replace(/\.\/favicon\.svg/g, `${base}favicon.svg`)
43
+ .replace(/\.\/gongan\.png/g, `${base}gongan.png`)
44
+ .replace(
45
+ '</head>',
46
+ ` <script>window.__NOTES_SITE__ = ${inline}</script>\n</head>`,
47
+ )
48
+ }
package/src/scan.mjs ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * 扫描笔记目录,产出:
3
+ * mdFiles / imageFiles 文件清单
4
+ * roots 导航树(category 节点;主站分组由 build 阶段再包一层 group)
5
+ * flat 阅读顺序的扁平索引
6
+ *
7
+ * 目录约定:顶层目录 = 分类,子目录任意深度;根目录散落笔记归入「概览」。
8
+ * 编号前缀(NN-)只影响排序,生成展示标题时会被去掉。
9
+ */
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import crypto from 'node:crypto'
13
+
14
+ export const IMAGE_EXT = /\.(png|jpe?g|gif|svg|webp|bmp)$/i
15
+
16
+ export const toPosix = (p) => p.split(path.sep).join('/')
17
+
18
+ const shortHash = (s) => crypto.createHash('md5').update(s).digest('hex').slice(0, 6)
19
+
20
+ export function naturalCompare(a, b) {
21
+ const num = (s) => {
22
+ const m = s.match(/^(\d+)/)
23
+ return m ? parseInt(m[1], 10) : null
24
+ }
25
+ const na = num(a)
26
+ const nb = num(b)
27
+ if (na !== null && nb !== null && na !== nb) return na - nb
28
+ return a.localeCompare(b, 'zh-CN', { numeric: true, sensitivity: 'base' })
29
+ }
30
+
31
+ /** 目录名 -> 标题:去掉 "NN-" 前缀 */
32
+ export function dirTitle(name) {
33
+ return name.replace(/^\d+\s*[-_]\s*/, '').trim() || name
34
+ }
35
+
36
+ /** 文件名 -> 标题:去掉 "NN-" 前缀与扩展名 */
37
+ export function fileTitle(rel) {
38
+ const base = path.posix.basename(rel, path.posix.extname(rel))
39
+ return base.replace(/^\d+\s*[-_]\s*/, '').trim() || base
40
+ }
41
+
42
+ /** 文章 id -> 产物文件名(ASCII 段 + 稳定短哈希,避免中文文件名问题) */
43
+ export function articleSlug(id) {
44
+ const ascii = id
45
+ .replace(/[^\x00-\x7f]+/g, '')
46
+ .replace(/[^\w.-]+/g, '-')
47
+ .replace(/-{2,}/g, '-')
48
+ .replace(/^-+|-+$/g, '')
49
+ return `${ascii || 'note'}-${shortHash(id)}`
50
+ }
51
+
52
+ export function makeArticleNode(rel) {
53
+ const id = rel.replace(/\.md$/i, '')
54
+ return {
55
+ type: 'article',
56
+ id,
57
+ title: fileTitle(rel),
58
+ file: `content/articles/${articleSlug(id)}.json`,
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @param {string} contentDir 笔记根目录
64
+ * @param {{ exclude?: string[], rootSkip?: string[], rootSectionTitle?: string }} options
65
+ */
66
+ export function scanNotes(contentDir, options = {}) {
67
+ const exclude = new Set(options.exclude || [])
68
+ const rootSkip = new Set((options.rootSkip || []).map((s) => String(s).toLowerCase()))
69
+ const rootSectionTitle = options.rootSectionTitle || '概览'
70
+
71
+ const mdFiles = [] // { abs, rel }
72
+ const imageFiles = [] // { abs, rel, base }
73
+
74
+ function walk(absDir, relDir) {
75
+ const entries = fs.readdirSync(absDir, { withFileTypes: true })
76
+ for (const e of entries) {
77
+ if (e.name.startsWith('.') || exclude.has(e.name)) continue
78
+ const abs = path.join(absDir, e.name)
79
+ const rel = relDir ? `${relDir}/${e.name}` : e.name
80
+ if (e.isDirectory()) {
81
+ walk(abs, rel)
82
+ } else if (e.isFile()) {
83
+ if (e.name.toLowerCase().endsWith('.md')) {
84
+ mdFiles.push({ abs, rel: toPosix(rel) })
85
+ } else if (IMAGE_EXT.test(e.name)) {
86
+ imageFiles.push({ abs, rel: toPosix(rel), base: e.name })
87
+ }
88
+ }
89
+ }
90
+ }
91
+ walk(contentDir, '')
92
+ mdFiles.sort((a, b) => naturalCompare(a.rel, b.rel))
93
+ imageFiles.sort((a, b) => naturalCompare(a.rel, b.rel))
94
+
95
+ // 递归构建分类节点:子目录在前,文章在后
96
+ function buildCategory(relDir) {
97
+ const abs = path.join(contentDir, relDir)
98
+ const entries = fs.readdirSync(abs, { withFileTypes: true })
99
+ const dirs = []
100
+ const files = []
101
+ for (const e of entries) {
102
+ if (e.name.startsWith('.') || exclude.has(e.name)) continue
103
+ if (e.isDirectory()) {
104
+ if (e.name.toLowerCase() !== 'images') dirs.push(e.name)
105
+ } else if (e.isFile() && e.name.toLowerCase().endsWith('.md')) {
106
+ files.push(e.name)
107
+ }
108
+ }
109
+ dirs.sort(naturalCompare)
110
+ files.sort(naturalCompare)
111
+
112
+ const children = []
113
+ for (const d of dirs) {
114
+ const sub = buildCategory(path.posix.join(relDir, d))
115
+ if (sub.children.length > 0) {
116
+ children.push({ type: 'subcategory', id: sub.id, title: sub.title, children: sub.children })
117
+ }
118
+ }
119
+ for (const f of files) {
120
+ children.push(makeArticleNode(path.posix.join(relDir, f)))
121
+ }
122
+ return { type: 'category', id: relDir, title: dirTitle(path.posix.basename(relDir)), children }
123
+ }
124
+
125
+ const roots = []
126
+
127
+ // 根目录散落笔记 → 「概览」
128
+ const rootNotes = mdFiles.filter(
129
+ (f) => !f.rel.includes('/') && !rootSkip.has(path.posix.basename(f.rel).toLowerCase()),
130
+ )
131
+ if (rootNotes.length > 0) {
132
+ roots.push({
133
+ type: 'category',
134
+ id: `00-${rootSectionTitle}`,
135
+ title: rootSectionTitle,
136
+ children: rootNotes.map((f) => makeArticleNode(f.rel)),
137
+ })
138
+ }
139
+
140
+ // 顶层目录 → 分类
141
+ const topDirs = fs
142
+ .readdirSync(contentDir, { withFileTypes: true })
143
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.') && !exclude.has(e.name))
144
+ .map((e) => e.name)
145
+ .sort(naturalCompare)
146
+
147
+ for (const d of topDirs) {
148
+ const node = buildCategory(d)
149
+ if (node.children.length > 0) roots.push(node)
150
+ }
151
+
152
+ return { mdFiles, imageFiles, roots, rootSectionTitle }
153
+ }
154
+
155
+ /** 按阅读顺序(深度优先,目录在前文章在后)摊平树 */
156
+ export function flattenRoots(roots) {
157
+ const flat = []
158
+ const walk = (node) => {
159
+ if (node.type === 'article') {
160
+ flat.push({ id: node.id, title: node.title, file: node.file })
161
+ } else if (node.children) {
162
+ node.children.forEach(walk)
163
+ }
164
+ }
165
+ roots.forEach(walk)
166
+ return flat
167
+ }
168
+
169
+ /**
170
+ * 主站菜单:在分类之上再包一层 group(如 16 个目录归入 4 个类别)。
171
+ * 未命中分组的目录、以及「概览」保持顶层。
172
+ */
173
+ export function applyMenuGroups(roots, groups) {
174
+ const byId = new Map(roots.map((r) => [r.id, r]))
175
+ const used = new Set()
176
+ const out = []
177
+
178
+ const overview = roots.find((r) => r.id.startsWith('00-'))
179
+ if (overview) {
180
+ out.push(overview)
181
+ used.add(overview.id)
182
+ }
183
+
184
+ groups.forEach((g, i) => {
185
+ const children = g.dirs.map((d) => byId.get(d)).filter(Boolean)
186
+ children.forEach((c) => used.add(c.id))
187
+ if (children.length > 0) {
188
+ out.push({ type: 'group', id: `group-${i}`, title: g.title, children })
189
+ }
190
+ })
191
+
192
+ for (const r of roots) {
193
+ if (!used.has(r.id)) out.push(r)
194
+ }
195
+ return out
196
+ }
package/src/server.mjs ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 极简静态服务器(dev 预览用),无第三方依赖。
3
+ * 支持 SPA 兜底:无扩展名的路径回退到 index.html。
4
+ */
5
+ import http from 'node:http'
6
+ import fs from 'node:fs'
7
+ import path from 'node:path'
8
+
9
+ const MIME = {
10
+ '.html': 'text/html; charset=utf-8',
11
+ '.js': 'text/javascript; charset=utf-8',
12
+ '.css': 'text/css; charset=utf-8',
13
+ '.json': 'application/json; charset=utf-8',
14
+ '.svg': 'image/svg+xml',
15
+ '.png': 'image/png',
16
+ '.jpg': 'image/jpeg',
17
+ '.jpeg': 'image/jpeg',
18
+ '.gif': 'image/gif',
19
+ '.webp': 'image/webp',
20
+ '.woff': 'font/woff',
21
+ '.woff2': 'font/woff2',
22
+ '.ttf': 'font/ttf',
23
+ '.ico': 'image/x-icon',
24
+ }
25
+
26
+ export function startServer({ dir, port = 4321, host = '127.0.0.1' }) {
27
+ const root = path.resolve(dir)
28
+
29
+ const server = http.createServer((req, res) => {
30
+ const urlPath = decodeURIComponent((req.url || '/').split('?')[0])
31
+ let file = path.resolve(root, `.${path.posix.normalize(urlPath)}`)
32
+
33
+ // 防目录穿越
34
+ if (!file.startsWith(root)) {
35
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' }).end('Forbidden')
36
+ return
37
+ }
38
+
39
+ if (fs.existsSync(file) && fs.statSync(file).isDirectory()) {
40
+ file = path.join(file, 'index.html')
41
+ }
42
+ if (!fs.existsSync(file)) {
43
+ // 无扩展名路径回退首页(hash 路由本身不需要,但避免直接访问子路径 404)
44
+ file = path.join(root, 'index.html')
45
+ }
46
+
47
+ fs.readFile(file, (err, data) => {
48
+ if (err) {
49
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }).end('Not Found')
50
+ return
51
+ }
52
+ res.writeHead(200, {
53
+ 'Content-Type': MIME[path.extname(file).toLowerCase()] || 'application/octet-stream',
54
+ 'Cache-Control': 'no-cache',
55
+ })
56
+ res.end(data)
57
+ })
58
+ })
59
+
60
+ server.listen(port, host, () => {
61
+ console.log(`\n🌐 本地预览:http://${host}:${port}/\n`)
62
+ })
63
+ return server
64
+ }
package/src/sync.mjs ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 笔记源同步:local(复制本地目录)或 git(浅克隆 / fetch+reset)到 workspace。
3
+ */
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ import { execSync } from 'node:child_process'
7
+
8
+ function run(cmd, cwd) {
9
+ // 非交互环境下避免 git 卡在凭证提示,直接失败
10
+ execSync(cmd, { cwd, stdio: 'inherit', env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } })
11
+ }
12
+
13
+ export function syncSource(config) {
14
+ const { source, root, content } = config
15
+ if (!source) return { skipped: true }
16
+
17
+ const workspace = path.resolve(root, source.workspace)
18
+
19
+ if (source.mode === 'local') {
20
+ const ignore = new Set(content.exclude || [])
21
+ if (!fs.existsSync(source.dir)) throw new Error(`本地笔记目录不存在:${source.dir}`)
22
+ fs.rmSync(workspace, { recursive: true, force: true })
23
+ fs.mkdirSync(workspace, { recursive: true })
24
+ const entries = fs.readdirSync(source.dir, { withFileTypes: true })
25
+ let copied = 0
26
+ for (const e of entries) {
27
+ if (ignore.has(e.name)) continue
28
+ fs.cpSync(path.join(source.dir, e.name), path.join(workspace, e.name), { recursive: true })
29
+ copied++
30
+ }
31
+ return { mode: 'local', from: source.dir, to: workspace, copied }
32
+ }
33
+
34
+ const { url, branch, depth } = source.git
35
+ const depthArg = depth ? `--depth ${depth}` : ''
36
+ const isRepo = fs.existsSync(path.join(workspace, '.git'))
37
+ if (!isRepo) {
38
+ fs.rmSync(workspace, { recursive: true, force: true })
39
+ run(`git clone ${depthArg} --branch ${branch} -- ${url} ${workspace}`, root)
40
+ } else {
41
+ run(`git fetch ${depthArg} origin ${branch}`, workspace)
42
+ run('git reset --hard FETCH_HEAD', workspace)
43
+ }
44
+ return { mode: 'git', from: url, to: workspace, branch }
45
+ }