寒冰是喵喵
欢迎来到我的小世界

Markdown 边注、术语、聊天框与交互组件测试

返回主页

这篇文章既是本站 Markdown 扩展的使用说明,也是文章边注、外部引用、术语原文、聊天记录、函数图像、钢琴窗与五线谱的渲染测试,当前由 MDX 渲染。

MDX 组件

本站将复杂的富文本能力注册为全局 MDX 组件。文章可以直接使用手绘标注、术语、边注、聊天框、函数图像、钢琴窗和五线谱组件,无需在每篇文章中重复导入。普通 Markdown 继续使用 Astro 默认渲染器,适合不需要组件的内容。

MDX 手绘标注

Rough Notation 的七种效果分别封装为 <Underline><Box><Circle><Highlight><StrikeThrough><CrossedOff><Bracket>

<Underline>下划线</Underline>
<Box>方框</Box>
<Circle>圆圈</Circle>
<Highlight>高亮</Highlight>
<StrikeThrough>删除线</StrikeThrough>
<CrossedOff>交叉划除</CrossedOff>
这句话中,<Bracket brackets={['left', 'right']}>只有这一小部分</Bracket> 会被标注。

段落前文 <Bracket class="inline-block w-52 align-middle" brackets={['left', 'right']}>这段较长的内容会自然换行,并由同一组括号包围</Bracket> 段落后文。

<Bracket class="block" brackets={['left', 'right']}>
  这是一整个被括号包裹的段落。它可以自然换行,并始终只绘制一组左右括号。
</Bracket>
这是手绘下划线
这是方框
这是圆圈
这是高亮
这是删除线
这是交叉划除
这是一整句话,其中 只有这一小部分 会被标注。
段落前文 这段较长的内容会自然换行,并由同一组括号包围 段落后文。
这是一整个被括号包裹的段落。它使用可用的正文宽度自然换行,并始终只绘制一组左右括号。

所有组件都支持 colorstrokeWidthiterationspaddinganimationDurationanimatemultilinertlclass<Bracket> 额外支持 bracketsgapbrackets 可选择 leftrighttopbottomgap 默认是 0.75em,用于为句内括号预留左右空间,可传入像素数字、任意 CSS 长度,或用 gap={0} 关闭。它默认将多行内容视为一个整体,避免为每一行重复绘制括号。无 JavaScript 时会保留对应的 CSS 降级效果;减少动态效果模式下会自动关闭绘制动画。

术语原文

MDX 页面可以使用全局注册的 <Term> 组件,通过 original 保留术语原文,并用 lang 标记原文语言:

<Term original="virtual memory" lang="en">
  虚拟内存
</Term>
<Term original="上下文切换" lang="zh-CN">
  context switch
</Term>

例如,操作系统中的 虚拟内存virtual memory 并不等同于物理内存;Rust 的 借用检查器borrow checker 则会在编译期检查引用是否有效。

标注也可以反过来使用。例如在英文段落中,context switch上下文切换 上方显示的是中文原词。lang 可以省略,但在中英文混排时建议保留。

<Term> 生成标准 HTML <ruby><rt> 元素。<rt> 可用于发音、翻译、音译或短注释。若内容需要离开本站的 Markdown pipeline,也可以直接使用标准 HTML,前提是目标渲染器允许 Markdown 中的 raw HTML:

<ruby>虚拟内存<rt lang="en">virtual memory</rt></ruby>

文章附记

<Sidenote> 的内容就是卡片正文,紧跟其后的段落用 <SidenoteText> 标记对应文字。不再需要额外的卡片组、ID 或配对名称:

<Sidenote>这里是附记,也支持普通的 **Markdown**。</Sidenote>

<p>
  <SidenoteText>这里是与附记相关的正文。</SidenoteText>
</p>

这段正文用来观察附记的位置和颜色关系。鼠标停在便签或这段文字上时,文字背景会显示相同颜色。边注适合补充背景、出处或不影响主线的细节;如果内容是理解文章所必需的,就不应该藏在边注里。

自定义标题与 Markdown

title 可以修改卡片标题;组件内容支持强调、代码、链接和多个段落:

这段正文用于检查自定义标题、富文本和多段内容。

嵌套为卡片堆

在一条 <Sidenote> 中继续嵌套 <Sidenote>,就会按出现顺序组成卡片堆。对应的 <SidenoteText> 使用相同顺序嵌套:第一张卡片对应外层文字,后面的卡片对应内层文字。

<Sidenote title="整句">
  第一张附记对应整句话。

  <Sidenote title="局部">第二张附记只对应其中一部分。</Sidenote>
</Sidenote>

<p>
  <SidenoteText>
    这是整句话,其中<SidenoteText>这一部分</SidenoteText>另有一张附记。
  </SidenoteText>
</p>

这句话同时关联了 背景和出处,也包含 延伸阅读

每张卡片保留自己的颜色和文字范围;点击当前卡片可以切换到下一张,把鼠标移到对应文字上也会切换到那张卡片。

外部资料引用

PDF Quote

<PDFQuote> 用来引用外部 PDF 或文献。href 应直接包含原文的页码或章节定位,locator 把这个位置显示给读者。可以把较长的原文片段直接放进引用框:前后文默认使用较浅的文字色,真正引用的部分用 <PDFQuoteText> 标出并恢复为正文颜色。

<PDFQuoteText> 是行内组件,不要求独占一个段落。它可以只包住半句话、完整的一句话,也可以包住会随行宽自然换行的连续文字。<SidenoteText> 仍可嵌在其中关联边注:

<PDFQuote
  href="https://example.com/paper.pdf#page=12"
  title="示例论文标题"
  source="作者 · 会议或出版物 · 年份"
  locator="§3.2 · p.12"
>
  这是第一段前文。它交代这一节正在处理的问题、作者采用的定义,以及接下来判断所依赖的条件。

第二段前文继续保留推理过程。真正需要引用的内容不必另起一段,也可以直接出现在一段上下文中。

<Sidenote title="译注">这条边注只解释真正引文中被标记的部分。</Sidenote>

作者在列出几个例外之后总结道: <PDFQuoteText>这是文章实际需要引用的一句话,其中的 <SidenoteText>关键术语或推理步骤</SidenoteText> 仍然可以关联边注;当句子较长时,它会跟随引用框的行宽自然换行。</PDFQuoteText> 随后作者开始说明这个结论不能覆盖的情况。

这里是第一段后文,它补充结论的适用范围,并解释下一节为什么要换用另一种分析方法。

最后再保留一小段后文,让读者能判断这句话在原资料中的位置和作用。

</PDFQuote>
PDF Quote§3.2 · p.12
示例论文标题作者 · 会议或出版物 · 年份

作者先回顾上一节得到的结果,然后限定这一节所讨论的问题。这里保留完整的过渡,是为了避免抽出一句话后改变原意。

接下来作者比较了两种情形,并逐步引出本文真正需要引用的判断。前文虽然不是引用重点,但仍为这句话提供必要条件。

在完成上述比较后,作者写道: 这只是文章实际引用的一句话,其中的 关键术语或推理步骤 仍然可以继续关联边注;整句话即使跨越多行,也保持为同一个强调范围。 原文随后转向这一判断的限制条件。

第一段后文给出一个不满足条件的反例,提醒读者不要把前面的结论推广到所有情况。

第二段后文说明作者接下来如何处理这个反例。它们以较浅的颜色保留,因此不会抢走真正引文的视觉重点。

点击顶部标题会保留作者传入的完整定位;外部地址在新标签页打开,站内来源则直接跳转。

GitHub Quote

GitHub 代码引用只需要一个带行号范围的 <GitHubQuote>。组件会在构建时拉取原始文件,自动识别仓库、分支、文件名、语言和行号,并在引用范围前后各保留 5 行上下文。拉取结果默认缓存 24 小时,缓存过期后会向 GitHub 重新验证;可以用 contextLines 调整上下文行数,用 cacheHours 调整缓存时长,用 lang 覆盖自动推断的语言:

<GitHubQuote
  href="https://github.com/user/repo/blob/main/src/task.ts#L44-L46"
  contextLines={5}
  cacheHours={24}
/>
GitHub Quotesrc/components/mdx/sidenote/sidenotes.ts · L78–L96
sidenotes.tshanbings/hanbings.github.io · main

  const overhang = stackPlacements[Math.min(cards.length - 1, stackPlacements.length - 1)].y
  stack.style.setProperty('--sidenote-stack-overhang', `${overhang}rem`)
  let activeIndex = 0

  const showCard = (nextIndex: number) => {
    activeIndex = (nextIndex + cards.length) % cards.length
    stack.dataset.sidenoteActive = String(activeIndex)

    cards.forEach((card, index) => {
      const depth = (index - activeIndex + cards.length) % cards.length
      const active = depth === 0
      card.classList.toggle('is-active', active)
      card.tabIndex = active ? 0 : -1
      card.toggleAttribute('inert', !active)
      if (active) card.removeAttribute('aria-hidden')
      else card.setAttribute('aria-hidden', 'true')
      setCardDepth(card, depth, cards.length)
    })

    anchors.forEach((anchor, index) => {
      anchor.classList.toggle('is-active', index === activeIndex)
    })
  }

  const showAnchorCard = (target: EventTarget | null) => {
    if (!(target instanceof Element)) return
    const anchor = target.closest<HTMLElement>('[data-sidenote-text]')
    if (!anchor || !textContainer?.contains(anchor)) return

函数图像

<FunctionPlot> 使用接近普通数学书写的表达式绘制函数。单个函数可以直接通过 expression 传入:

<FunctionPlot
  title="正弦函数"
  expression="sin(x)"
  xDomain={[-2 * Math.PI, 2 * Math.PI]}
  yDomain={[-1.5, 1.5]}
/>
正弦函数点击图像后可用滚轮缩放
正弦函数,横轴范围 -6.283185307179586 到 6.283185307179586,纵轴范围 -1.5 到 1.5,绘制 y = sin(x)。可拖动平移;点击图像后可使用滚轮缩放。x = — · y = —
y = sin(x)

拖动画布可以平移;点击函数图像后,滚轮才会被图像捕获并围绕指针位置缩放,点击外部或按下 Escape 可以退出。“重置视图”会恢复文章中设置的坐标范围。需要对比多个函数时,使用 functions 数组:

<FunctionPlot
  title="正弦与余弦"
  functions={[
    {expression: 'sin(x)', label: 'y = sin(x)'},
    {expression: 'cos(x)', label: 'y = cos(x)'},
  ]}
  xDomain={[-2 * Math.PI, 2 * Math.PI]}
  yDomain={[-1.5, 1.5]}
  height={320}
/>
正弦与余弦点击图像后可用滚轮缩放
正弦与余弦,横轴范围 -6.283185307179586 到 6.283185307179586,纵轴范围 -1.5 到 1.5,绘制 y = sin(x)、y = cos(x)。可拖动平移;点击图像后可使用滚轮缩放。x = — · y = —
y = sin(x)y = cos(x)

表达式支持 +-*/%^,变量 x,常量 piπe,以及常见的三角、反三角、双曲、指数、对数、取整和绝对值函数。乘号可以省略,因此 2x2sin(x)(x + 1)(x - 1) 都能直接使用。每张图最多绘制六个函数;表达式、坐标范围或高度无效时,构建会直接报错。

钢琴窗

MDX 页面可以使用全局注册的 <Piano> 组件。它使用科学音高记号描述按下的琴键,例如中央 C 是 C4,升 C 可以写成 C#4C♯4,降 D 也可以写成 Db4

<Piano title="C 大调和弦" notes="C4 E4 G4" />
C 大调和弦
按下:C4、E4、G4

notes 可以传入字符串或字符串数组,并使用空格、逗号或顿号分隔。浏览者可以点击琴键切换按下状态,钢琴窗会实时列出当前音符;“清空”按钮会释放全部琴键。

钢琴窗默认从 C4 开始显示两个八度。可以用 octave 指定起始八度,用 octaves 指定一到四个八度:

<Piano title="低音区的 F 小调和弦" notes="F2 Ab2 C3" octave={2} octaves={2} />
低音区的 F 小调和弦
按下:F2、G♯2、C3

降号会被规范到对应的升号琴键,因此上例中的 Ab2 显示为 G♯2。如果音名无法识别,或音符不在当前琴窗的音域内,构建会直接报错。省略 notes 则会生成一架没有预先按下琴键的钢琴:

<Piano title="试着按下几个音" />
试着按下几个音
尚未按下音符

五线谱

<MusicScore> 使用 ABC 记谱文本生成响应式五线谱。例如,下面是一个八度的 C 大调音阶:

<MusicScore
  title="C 大调音阶"
  abc={`X:1
M:4/4
L:1/4
K:C
C D E F | G A B c |]`}
/>
C 大调音阶
X:1
M:4/4
L:1/4
K:C
C D E F | G A B c |]

M: 表示拍号,L: 表示默认音符时值,K: 表示调号。大写音名位于较低的八度,小写音名位于较高的八度;升号写作 ^,降号写作 _。乐谱默认随文章宽度缩放,也可以用 responsive={false} 关闭,或用 staffWidth={640} 指定谱面宽度。JavaScript 不可用或内容进入 RSS 时,组件会保留 ABC 源码。

聊天记录

<ChatBox> 用来包住一段可以折叠的聊天记录,每一条消息使用 <ChatMessage>role="humen" 的消息显示在右侧,role="ai" 的消息显示在左侧:

<ChatBox title="关于 MDX 组件的讨论">
  <ChatAI slot="ai" name="OpenAI Codex" />

  <ChatMessage role="humen" time="10:24">
    可以在消息里使用 **Markdown** 吗?
  </ChatMessage>
  <ChatMessage role="ai" time="10:24">
    可以,其他 MDX 组件也可以正常嵌套。
  </ChatMessage>
</ChatBox>
关于 MDX 组件的讨论OpenAI Codex 的对话 · 聊天窗内 AI 内容未经验证

可以在消息里使用 Markdown 和边注吗?

可以。比如下面这些内容都可以正常显示:

这段 AI 消息正文与外面的边注相互关联。

很好,这样我就能直接展示完整的对话了。

聊天窗口默认展开,点击标题栏即可折叠;传入 open={false} 可以让它初始保持折叠。<ChatAI slot="ai" name="…"> 用来声明参与对话的 AI;省略时显示通用的 AI 提示。标题下方会明确说明 AI 内容未经验证,文章正文才是人类内容。聊天内容中的标题不会混入文章目录。消息气泡使用一套独立于文章正文的紧凑 Markdown 排版。

边注应作为 <ChatBox> 的直接子元素,紧跟在它所关联的 <ChatMessage> 之前;聊天框里的每条边注都会自动标记为“人类编写或已验证的信息”。宽屏时边注会浮到聊天窗口外侧,窄屏时则留在窗口内、消息气泡之外。折叠时消息和边注都会隐藏,只保留标题栏。两位对话人只通过左右位置和气泡颜色区分,不显示头像或昵称;time 和对应的 datetime 都是可选项。

窄屏、RSS 与无障碍

<Sidenote> 生成 <aside role="note">;嵌套卡片与嵌套正文按出现顺序配对。在手机或较窄的窗口中,单条附记会成为带有标题的正文块,多条附记仍可逐张切换;术语原文则继续显示在术语上方。聊天框会缩小内边距,并保持两位对话人的左右关系。

钢琴窗中的每个琴键都是带有按下状态的按钮,可以通过键盘聚焦并操作。琴键较多或屏幕较窄时,键盘区域可以横向滚动;RSS 中则保留标题与初始音符文字,不依赖交互脚本。

RSS 使用同一套内容组件,并保留每一张卡片对应的 <aside><ruby><rt> 语义;所有附记内容都会依次出现,因此不会依赖客户端交互脚本。

相关规范与文档

寒冰还有 11 篇 draft 中的文章,谴责它摸鱼!