@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/src/markdown.mjs
ADDED
|
@@ -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(/&/g, '&')
|
|
55
|
+
.replace(/</g, '<')
|
|
56
|
+
.replace(/>/g, '>')
|
|
57
|
+
.replace(/"/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, '&')
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/"/g, '"')
|
|
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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|