93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
// wiki 知識庫的純邏輯:目錄解析與渲染、頁名、驗證。讀寫與 git 在 register.tsx。
|
||
|
||
export type Entry = { domain: string; topic: string }
|
||
|
||
export const CACHE_MS = 10 * 60 * 1000
|
||
|
||
// 指令名稱只能用英數與底線,領域與主題也沿用。
|
||
const NAME_RE = /^[A-Za-z0-9_]+$/
|
||
|
||
export const validName = (s: string): boolean => NAME_RE.test(s)
|
||
|
||
export const commandName = (e: Entry): string => `${e.domain}-${e.topic}`
|
||
|
||
export const pageName = (e: Entry): string => `${e.domain}-${e.topic}`
|
||
|
||
// Home 的格式:「## 領域」章、「- [主題](頁名)」節。
|
||
export const parseCatalog = (home: string): Entry[] => {
|
||
const out: Entry[] = []
|
||
let domain = ''
|
||
for (const line of home.split('\n')) {
|
||
const h = /^##\s+(.+?)\s*$/.exec(line)
|
||
if (h) {
|
||
domain = h[1]!
|
||
continue
|
||
}
|
||
const m = /^\s*[-*]\s+\[(.+?)\]\((.+?)\)\s*$/.exec(line)
|
||
if (m && domain) out.push({ domain, topic: m[1]! })
|
||
}
|
||
return out
|
||
}
|
||
|
||
const group = (entries: Entry[]): Map<string, string[]> => {
|
||
const g = new Map<string, string[]>()
|
||
for (const e of entries) g.set(e.domain, [...(g.get(e.domain) ?? []), e.topic])
|
||
return g
|
||
}
|
||
|
||
export const addEntry = (entries: Entry[], e: Entry): Entry[] =>
|
||
entries.some(x => x.domain === e.domain && x.topic === e.topic) ? entries : [...entries, e]
|
||
|
||
export const renderHome = (entries: Entry[]): string => {
|
||
const lines = ['# 知識庫', '']
|
||
for (const [domain, topics] of group(entries)) {
|
||
lines.push(`## ${domain}`, '')
|
||
for (const t of topics) lines.push(`- [${t}](${pageName({ domain, topic: t })})`)
|
||
lines.push('')
|
||
}
|
||
return lines.join('\n')
|
||
}
|
||
|
||
export const renderSidebar = (entries: Entry[]): string => {
|
||
const lines = ['**[知識庫](Home)**', '']
|
||
for (const [domain, topics] of group(entries)) {
|
||
lines.push(`**${domain}**`, '')
|
||
for (const t of topics) lines.push(`- [${t}](${pageName({ domain, topic: t })})`)
|
||
lines.push('')
|
||
}
|
||
return lines.join('\n')
|
||
}
|
||
|
||
export const renderPage = (e: Entry, body: string, source: string): string =>
|
||
`# ${e.topic}\n\n> 領域:${e.domain}\n\n${body.trim()}\n\n---\n\n出處:${source}\n`
|
||
|
||
export const commitMessage = (e: Entry, existed: boolean): string =>
|
||
`feat: ${existed ? '更新' : '新增'}知識「${e.domain}/${e.topic}」`
|
||
|
||
// tea 設定檔(YAML)中,找出網址主機相符的登入的 token。
|
||
export const teaToken = (yaml: string, host: string): string | undefined => {
|
||
for (const block of yaml.split(/^\s*-\s+name:/m).slice(1)) {
|
||
const url = /^\s*url:\s*(\S+)/m.exec(block)?.[1]
|
||
const token = /^\s*token:\s*(\S+)/m.exec(block)?.[1]
|
||
if (!url || !token) continue
|
||
try {
|
||
if (new URL(url.replace(/^['"]|['"]$/g, '')).host === host) return token.replace(/^['"]|['"]$/g, '')
|
||
} catch {}
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
export const wikiUrlFromRemote = (remote: string): string | undefined => {
|
||
const base = remote.trim().replace(/\.git$/, '')
|
||
return base ? `${base}.wiki.git` : undefined
|
||
}
|
||
|
||
// 指令參數:第一行「領域 主題 [出處]」,其後為內容。
|
||
export const parseWriteArgs = (args: string): { entry: Entry; source: string; body: string } | undefined => {
|
||
const [first = '', ...rest] = args.trim().split('\n')
|
||
const [domain, topic, ...src] = first.trim().split(/\s+/)
|
||
const body = rest.join('\n').trim()
|
||
if (!domain || !topic || !body || !validName(domain) || !validName(topic)) return undefined
|
||
return { entry: { domain, topic }, source: src.join(' ') || '使用者提供', body }
|
||
}
|