feat: 零点原核频道v1+登录双门地基+五湖开场——zero_point裁决链/灯塔查号/code_repo_login/通知卡重设计/浅色主题联动(冰朔20260815夜谕)
This commit is contained in:
parent
ffed065841
commit
07fd4c85ef
16 changed files with 3082 additions and 88 deletions
|
|
@ -0,0 +1,146 @@
|
|||
//! 知识渲染件 · 渲染入口(常驻模块 · 插座口 knowledge-render)
|
||||
//!
|
||||
//! 职责:把仓库里的 Markdown 文件变成可读的页面——frontmatter 解析、
|
||||
//! Notion/Outline 导出兼容、标题锚点、大纲同源、跳转。
|
||||
import { useMemo } from 'react'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { marked, Parser, type Tokens } from 'marked'
|
||||
export { knowledgeRenderManifest } from './manifest'
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')
|
||||
}
|
||||
|
||||
export function splitFrontmatter(body: string) {
|
||||
const normalized = body.replace(/\r\n/g, '\n')
|
||||
if (!normalized.startsWith('---\n')) return { content: normalized, metadata: {} as Record<string, string>, tags: [] as string[] }
|
||||
const closing = normalized.indexOf('\n---\n', 4)
|
||||
if (closing < 0) return { content: normalized, metadata: {} as Record<string, string>, tags: [] as string[] }
|
||||
const metadata: Record<string, string> = {}
|
||||
const tags: string[] = []
|
||||
let pendingListKey = ''
|
||||
for (const line of normalized.slice(4, closing).split('\n')) {
|
||||
const listItem = /^\s+-\s+(.+)$/.exec(line)
|
||||
if (listItem && pendingListKey) {
|
||||
if (pendingListKey === 'tags') tags.push(listItem[1].trim().replace(/^['"]|['"]$/g, ''))
|
||||
continue
|
||||
}
|
||||
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
|
||||
if (!match) continue
|
||||
pendingListKey = match[2] === '' ? match[1] : ''
|
||||
const value = match[2].replace(/^['"]|['"]$/g, '')
|
||||
metadata[match[1]] = value
|
||||
if (match[1] === 'tags') {
|
||||
const inline = /^\[(.*)\]$/.exec(match[2].trim())
|
||||
const source = inline ? inline[1] : value
|
||||
source.split(',').map((part) => part.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean).forEach((tag) => tags.push(tag))
|
||||
}
|
||||
}
|
||||
return { content: normalized.slice(closing + 5), metadata, tags }
|
||||
}
|
||||
|
||||
function stripNotionLinks(line: string) {
|
||||
// Notion 页面链接 → 只留链接文字(跳转关系按谕旨失效)
|
||||
return line.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (whole, text: string, href: string) =>
|
||||
(/notion\.(?:so|site)/.test(href) ? text : whole))
|
||||
}
|
||||
|
||||
export function notionCompat(content: string) {
|
||||
// 存量旧库文件尚未洗净:渲染时按"进门即洗"同规矩兜底过滤一遍。
|
||||
// 内容一字不改,只把外来知识库的外壳换成光湖原生 Markdown(冰朔 2026-08-15 谕:不兼容,进门就转)。
|
||||
const result: string[] = []
|
||||
for (const line of content.replace(/\r\n/g, '\n').split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed === '<aside>' || trimmed === '</aside>') continue
|
||||
if (trimmed.startsWith(':::toggle')) {
|
||||
const title = trimmed.slice(':::toggle'.length).trim()
|
||||
result.push(title ? `**▸ ${title}**` : '**▸ 详情**')
|
||||
continue
|
||||
}
|
||||
if (trimmed === ':::' || trimmed.startsWith(':::toc')) continue
|
||||
result.push(stripNotionLinks(line.replace(/<br\s*\/?>/g, '\n')))
|
||||
}
|
||||
// Notion 粗体写法(**文字:**紧接正文)不合 CommonMark:给收尾星号补一口气
|
||||
return result.join('\n').replace(/(\*\*[^*\n]+\*\*)(?=[^\s.,;:!?,。;:!?、)\]"'])/g, '$1 ')
|
||||
}
|
||||
|
||||
const CALLOUT_TINTS: [RegExp, string][] = [
|
||||
[/📌|🌌|🧭|💜|🗂|🏷|📚/, 'callout-lavender'],
|
||||
[/💡|⚡|🌟|☀|✨|🔑/, 'callout-amber'],
|
||||
[/⚠|🔥|❗|🚨|❌/, 'callout-rose'],
|
||||
[/✅|🌿|🍀|💚|✔/, 'callout-mint'],
|
||||
[/🌊|💧|🔵|❄|🧊/, 'callout-sky'],
|
||||
]
|
||||
function normalizeTitle(value: string) {
|
||||
return value.replace(/[\s·•・\-_|*`]/g, '').toLowerCase()
|
||||
}
|
||||
|
||||
function calloutTint(text: string) {
|
||||
for (const [pattern, tint] of CALLOUT_TINTS) if (pattern.test(text)) return tint
|
||||
return 'callout-slate'
|
||||
}
|
||||
|
||||
export function markdownHtml(body: string, knownTitles: readonly string[] = []) {
|
||||
const compat = notionCompat(body)
|
||||
const titleMap = new Map<string, string>()
|
||||
for (const title of knownTitles) titleMap.set(normalizeTitle(title), title)
|
||||
const withWikiLinks = compat.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_whole, target: string, label?: string) =>
|
||||
`<button class="wiki-link" type="button" data-wiki="${escapeHtml(target.trim())}">${escapeHtml((label || target).trim())}</button>`)
|
||||
let headingIndex = 0
|
||||
const renderer = new marked.Renderer()
|
||||
renderer.heading = (token) => {
|
||||
const id = `heading-${headingIndex}`
|
||||
headingIndex += 1
|
||||
const inlineHtml = token.tokens.length ? Parser.parseInline(token.tokens) : escapeHtml(token.text)
|
||||
return `<h${token.depth} id="${id}">${inlineHtml}</h${token.depth}>\n`
|
||||
}
|
||||
renderer.blockquote = (token) => {
|
||||
const inner = Parser.parse(token.tokens) as string
|
||||
return `<blockquote class="callout ${calloutTint(token.raw)}">${inner}</blockquote>\n`
|
||||
}
|
||||
renderer.link = (token) => {
|
||||
const inner = token.tokens.length ? Parser.parseInline(token.tokens) : escapeHtml(token.text)
|
||||
const href = token.href || ''
|
||||
if (/notion\.(?:so|site)/i.test(href)) {
|
||||
// Notion 残留链接:认得出是自家页面就转成内部跳转,认不出标"外来页面"。
|
||||
const hit = titleMap.get(normalizeTitle(token.text))
|
||||
if (hit) return `<button class="wiki-link" type="button" data-wiki="${escapeHtml(hit)}">${inner}</button>`
|
||||
return `<a class="external-link" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}<span class="external-mark">外来页面</span></a>`
|
||||
}
|
||||
if (/^https?:\/\//i.test(href)) return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}</a>`
|
||||
return `<a href="${escapeHtml(href)}">${inner}</a>`
|
||||
}
|
||||
const html = marked.parse(withWikiLinks, { async: false, gfm: true, breaks: false, renderer }) as string
|
||||
return DOMPurify.sanitize(html, { ADD_ATTR: ['data-wiki', 'id', 'target', 'rel'], ADD_TAGS: ['button'] })
|
||||
}
|
||||
|
||||
export function documentOutline(body: string) {
|
||||
const headings: { id: string; level: number; title: string }[] = []
|
||||
const plain = (tokens: unknown): string => (tokens as { type: string; text?: string; tokens?: unknown[] }[])
|
||||
.map((token) => (token.tokens && token.tokens.length ? plain(token.tokens) : token.text || '')).join('')
|
||||
marked.lexer(splitFrontmatter(body).content, { gfm: true })
|
||||
.filter((token): token is Tokens.Heading => token.type === 'heading')
|
||||
.forEach((token, index) => {
|
||||
if (index >= 24) return
|
||||
headings.push({ id: `heading-${index}`, level: token.depth, title: plain(token.tokens).trim() })
|
||||
})
|
||||
return headings
|
||||
}
|
||||
|
||||
export function jumpToHeading(id: string) {
|
||||
const element = document.querySelector<HTMLElement>(`.document-scroll [id="${id}"]`)
|
||||
if (!element) return
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
element.classList.remove('heading-flash')
|
||||
void element.offsetWidth
|
||||
element.classList.add('heading-flash')
|
||||
}
|
||||
|
||||
export function MarkdownDocument({ body, knownTitles, onWiki }: { body: string; knownTitles?: readonly string[]; onWiki?: (target: string) => void }) {
|
||||
const parsed = useMemo(() => splitFrontmatter(body), [body])
|
||||
const html = useMemo(() => markdownHtml(parsed.content, knownTitles), [parsed.content, knownTitles])
|
||||
return <article className="markdown-document" onClick={(event) => {
|
||||
const element = (event.target as HTMLElement).closest<HTMLElement>('[data-wiki]')
|
||||
if (element?.dataset.wiki && onWiki) onWiki(element.dataset.wiki)
|
||||
}} dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
//! 模块形状声明 · 插座制(冰朔 2026-08-15 架构谕)
|
||||
//!
|
||||
//! 软件只认这一个插座形状:每个模块 = manifest(我是谁/插哪个口/从哪来)
|
||||
//! + index(渲染入口导出)。常驻模块住在频道里;将来 origin='repo' 的
|
||||
//! 模块躺代码仓库,人格体按需拉取部署。
|
||||
export const knowledgeRenderManifest = {
|
||||
moduleId: 'hololake.knowledge-render',
|
||||
name: '知识渲染件',
|
||||
version: '0.1.0',
|
||||
slot: 'knowledge-render',
|
||||
origin: 'resident',
|
||||
exports: ['splitFrontmatter', 'documentOutline', 'jumpToHeading', 'MarkdownDocument'],
|
||||
} as const
|
||||
|
||||
export type ModuleManifest = {
|
||||
moduleId: string
|
||||
name: string
|
||||
version: string
|
||||
slot: string
|
||||
origin: 'resident' | 'repo'
|
||||
exports: readonly string[]
|
||||
}
|
||||
Loading…
Reference in a new issue