/**
* [INPUT]: 依赖 react(useState)、lib/markdown 的 parseBlocks/parseInline/CALLOUT_RGB(语义单一真相源)、
* lucide-react(标注块 13 图标 + 折叠箭头)、course/ImageLightbox(图片放大);
* props: md(Markdown 文本)、onJump(lessonId) 视频跳转回调
* [OUTPUT]: 对外提供 MarkdownNotes 组件(课程笔记的 **v4 纸墨朱皮肤**)+ defaultNotes(无笔记时的模板)
* [POS]: components/v4 共享原语,视频页笔记区(screens/v4/video/NotesPane)消费。
* **本文件只负责「长什么样」**——「什么是表格/标注块/链接」的语义全在 lib/markdown,
* 与法律条款弹窗(course/MarkdownContent)共用同一份解析,语义修一处两处同得。
* 标注块配色=Obsidian 官方色板(老大定:笔记区是【内容】不是产品 UI,
* Obsidian 老用户零学习成本;产品 UI 仍严守纸墨朱)。
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
*/
import { useMemo, useState } from 'react'
import {
AlertTriangle, Bug, Check, CheckCircle2, ChevronRight, ClipboardList,
Flame, HelpCircle, Info, List, Pencil, Quote, X, Zap,
} from 'lucide-react'
import { parseBlocks, parseInline, CALLOUT_RGB } from '../../lib/markdown'
import { ImageLightbox } from '../course/ImageLightbox'
const MONO = 'var(--zzm-mono)'
const ACC = 'var(--zzm-accent,#A6402F)'
const INK = 'var(--zzm-ink,#161616)'
const TEXT = 'var(--zzm-text-2,#3F3F3B)'
const HAIR = 'var(--zzm-hairline-2,rgba(0,0,0,.09))'
// 标注块图标注册表:lib/markdown 的 icon 名(字符串)→ lucide 组件。
// 语义层不认识 React 故只存名字,映射落在渲染层——同 lib/siteIcons 既有范式。
const ICONS = {
Pencil, ClipboardList, Info, CheckCircle2, Flame, Check,
HelpCircle, AlertTriangle, X, Zap, Bug, List, Quote,
}
// ── 行内 token → JSX(4pt 基线:正文 13.5px → 行高 24px)──
function Inline({ text, onJump }) {
return parseInline(text).map((t, i) => {
switch (t.type) {
case 'strong':
return {t.value}
case 'code':
return (
{t.value}
)
case 'jump':
// 课节跳转胶囊(v4 产品核心):站内导航,非外链
return (
{ ev.preventDefault(); onJump?.(t.target) }}
style={{
display: 'inline-flex', alignItems: 'center', padding: '1px 9px', margin: '0 2px',
borderRadius: 999, background: 'rgba(166,64,47,.07)', color: ACC, fontSize: '.9em',
fontWeight: 600, textDecoration: 'none', cursor: 'pointer',
border: '1px solid rgba(166,64,47,.28)',
}}
>{t.text}
)
case 'link':
// href 为 null = 未过 safeHref 白名单(javascript: 等)→ 降级纯文本,绝不产出可点元素
if (!t.href) return {t.text}
return (
{t.text}
)
case 'image':
return t.src
?
: {t.alt}
default:
return {t.value}
}
})
}
// ── 标注块(Obsidian callout):左缘色条 + 淡色底 + 图标标题行 + 可折叠 ──
function Callout({ block, onJump, onZoom }) {
const [open, setOpen] = useState(block.fold !== '-') // '-' 默认折叠 / '+' 默认展开 / null 不可折叠
const rgb = CALLOUT_RGB[block.color] ?? CALLOUT_RGB.blue
const Icon = ICONS[block.icon] ?? Info
const foldable = block.fold !== null
const hasBody = block.children.length > 0
// ⚠️ 结构性保证:不可折叠的标注块【恒展开】,绝不看 open 态。
// 切课节时 MarkdownNotes 不卸载(NotesPane 无 key),若上一节同下标处是个被折起的标注块,
// 其 open=false 会残留到本节;本节若不可折叠就没有展开入口 → 正文永久看不见。
// 一条 `[!CAUTION] 数据丢失重灾区` 被永久藏起来是灾难级的,故这里不靠 key 侥幸,直接消除该可能。
const show = !foldable || open
return (
|
|
))}
|---|
|
|
))}
{b.content}
)
case 'table':
return ) case 'image': return b.src ? (