博客新功能:代码图标、源码引用、行情图表 !

展示这次给博客新增的代码块语言图标、inline code 主题、GitHub 源码和 diff 引用,以及 TradingView 行情图表短语法。

这次主要把博客的写作体验继续往前推了一步:代码块更容易扫读,代码引用能直接指向 GitHub,金融类内容可以用很短的语法插入 TradingView 图表。

先看新功能

代码块有语言图标

普通代码块现在也会有标题栏。左侧是语言图标,右侧仍然保留复制按钮;如果代码来自 GitHub,标题栏还会出现 GitHub 跳转入口。

TypeScript
const feature = '语言图标'
const theme = 'catppuccin'
console.log(feature, theme)

inline code 重新做了主题

正文里的 git log --follow$AAPL::tv AAPL 不再像一块突兀的亮色贴片。明亮模式下它更接近 Catppuccin Latte 的纸面色,黑暗模式下则压进 Mocha 的低亮度底色。

直接引用 GitHub 代码

写作时输入:

Markdown
::github-code repo="mizorewww/blog" ref="c067042276d4b4b384c66c0c61fcd4f6716eb599" path="contentlayer.config.ts" lines="251-253" lang="ts" title="contentlayer.config.ts"

渲染出来就是带 Shiki 高亮、语言图标和 GitHub 链接的代码块:

contentlayer.config.ts在 GitHub 查看代码
const githubEmbedPattern = /^::github-(code|diff)\s+(.+)$/
const tradingViewMiniPattern = /^\$([A-Za-z0-9._:-]+)$/
const tradingViewAdvancedPattern = /^::(?:tv|tv-advanced|tradingview)\s+(.+)$/

diff 像 GitHub 一样分行染色

写作时输入:

Markdown
::github-diff repo="mizorewww/blog" ref="c067042276d4b4b384c66c0c61fcd4f6716eb599" path="css/prism.css" lines="1-15"

渲染效果:

mizorewww/blog:css/prism.css在 GitHub 查看 diff
diff --git a/css/prism.css b/css/prism.css
index d655cf1..6cf79f8 100644
--- a/css/prism.css
+++ b/css/prism.css
@@ -8,6 +8,44 @@ figure[data-rehype-pretty-code-figure] [data-rehype-pretty-code-title] {
   @apply flex items-center justify-between gap-3 border-b border-slate-200 bg-white/70 px-4 py-2 text-xs text-slate-600 dark:border-[#405064] dark:bg-white/[0.035] dark:text-white/65;
 }
 
+figure[data-rehype-pretty-code-figure] .code-title-main {
+  @apply inline-flex min-w-0 items-center gap-2;
+}
+
+figure[data-rehype-pretty-code-figure] .code-language-icon {
+  @apply inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-[5px] border border-slate-300 bg-slate-100 px-1 font-sans text-[0.62rem] leading-none font-semibold text-slate-600 dark:border-white/15 dark:bg-white/10 dark:text-white/70;
+}

单行 ticker 自动变成 Mini Chart

写作时单独一行输入:

Markdown
$AAPL
$BINANCE:BTCUSDT.P

渲染效果:

Advanced Chart 也有短语法

写作时输入:

Markdown
::tv AAPL interval=60 height=460

渲染效果:

代码逻辑

短语法先在 remark 阶段变成组件

这两个正则负责识别行情图表写法。$AAPL 是 Mini Chart,::tv AAPL 是 Advanced Chart。

contentlayer.config.ts在 GitHub 查看代码
const githubEmbedPattern = /^::github-(code|diff)\s+(.+)$/
const tradingViewMiniPattern = /^\$([A-Za-z0-9._:-]+)$/
const tradingViewAdvancedPattern = /^::(?:tv|tv-advanced|tradingview)\s+(.+)$/

Mini Chart 的转换很克制:只有整段内容就是一个 ticker 时才会替换,避免正文里随手提到 $AAPL 就插入一个大图表。

contentlayer.config.ts在 GitHub 查看代码
function createTradingViewMiniNode(symbol: string): MdastNode | null {
  if (!isTradingViewTicker(symbol)) {
    return null
  }
 
  return createMdxFlowNode('TradingViewMiniChart', {
    symbol: normalizeTradingViewSymbol(symbol),
  })

Advanced Chart 多一步解析参数。比如 height=460interval=60 会作为 JSX attribute 传给组件。

contentlayer.config.ts在 GitHub 查看代码
function createTradingViewAdvancedNode(source: string): MdastNode | null {
  const [rawSymbol, ...attrParts] = source.trim().split(/\s+/)
 
  if (!rawSymbol || !isTradingViewTicker(rawSymbol)) {
    return null
  }
 
  const attrs = parseEmbedAttributes(attrParts.join(' ')) as TradingViewAttrs
 
  return createMdxFlowNode('TradingViewAdvancedChart', {
    height: attrs.height,
    interval: attrs.interval,
    locale: attrs.locale,
    symbol: normalizeTradingViewSymbol(rawSymbol),
    timezone: attrs.timezone,
  })

最后把插件挂进 Contentlayer 的 remark 链路里。这样短语法发生在 MDX 编译前,浏览器里不会再扫字符串。

contentlayer.config.ts在 GitHub 查看代码
export default makeSource({
  contentDirPath: 'data',
  documentTypes: [Blog, Authors],
  mdx: {
    cwd: process.cwd(),
    remarkPlugins: [remarkGfm, remarkTradingViewWidgets, remarkIconShortcodes, remarkGitHubEmbeds],
    rehypePlugins: [

TradingView 只在客户端加载

TradingView 官方 widget 需要插入外部 script,所以组件保持为 client component。渲染时先清空容器,再放入 widget 容器和配置 script;主题或参数变化时重新生成。

components/TradingViewWidgets.tsx在 GitHub 查看代码
  useEffect(() => {
    const container = containerRef.current
 
    if (!container) {
      return
    }
 
    container.innerHTML = '<div class="tradingview-widget-container__widget"></div>'
 
    const script = document.createElement('script')
    script.async = true
    script.src = scriptSrc
    script.text = JSON.stringify(config)
    container.appendChild(script)
 
    return () => {
      container.innerHTML = ''
    }
  }, [config, scriptSrc])

股票代码会先规范化。AAPL 默认映射成 NASDAQ:AAPL,如果你写 NYSE:IBM 这种完整 symbol,就不会改动。

lib/tradingview.ts在 GitHub 查看代码
export function normalizeTradingViewSymbol(value: string, defaultExchange = 'NASDAQ') {
  const symbol = value.trim().replace(/^\$/, '').toUpperCase()
 
  if (!symbol) {
    return ''
  }
 
  if (explicitTradingViewSymbolPattern.test(symbol)) {
    return symbol
  }
 
  if (plainTickerPattern.test(symbol)) {
    return `${defaultExchange}:${symbol}`
  }
 
  return symbol
}

代码块标题栏统一增强

代码块语言图标没有在每篇文章里手写,而是在 rehype 阶段统一补。它会先找到 Shiki 生成的 figure,再保证有一个标题栏。

contentlayer.config.ts在 GitHub 查看代码
function enhanceCodeTitle(node: HastNode) {
  if (
    node.tagName !== 'figure' ||
    node.properties?.['data-rehype-pretty-code-figure'] === undefined
  ) {
    return
  }
 
  const title = ensureCodeTitle(node)
  const code = findDescendantElement(node, 'code')
  const sourceUrl = typeof code?.data?.githubSourceUrl === 'string' ? code.data.githubSourceUrl : ''
  const language = getCodeLanguage(code)
  const titleText = getNodeText(title) || getLanguageName(language)
 
  title.children = [createCodeTitleNode(titleText, language)]
 
  if (sourceUrl) {
    title.children.push(createCodeSourceLinkNode(sourceUrl, language))
  }

标题栏左侧由语言图标和标题文本组成。没有显式 title 时,就回退到语言名,比如 TypeScriptDiffMarkdown

contentlayer.config.ts在 GitHub 查看代码
function createCodeTitleNode(titleText: string, language: string): HastNode {
  return {
    type: 'element',
    tagName: 'span',
    properties: { className: ['code-title-main'] },
    children: [
      createCodeLanguageIconNode(language),
      {
        type: 'element',
        tagName: 'span',
        properties: { className: ['code-title-text'] },
        children: [{ type: 'text', value: titleText || getLanguageName(language) }],
      },

视觉上用小尺寸 badge 表达语言,不去额外引入一整套语言 logo。这样代码块有识别度,也不会把首屏 JavaScript 变重。

css/prism.css在 GitHub 查看代码
figure[data-rehype-pretty-code-figure] .code-title-main {
  @apply inline-flex min-w-0 items-center gap-2;
}
 
figure[data-rehype-pretty-code-figure] .code-language-icon {
  @apply inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-[5px] border border-slate-300 bg-slate-100 px-1 font-sans text-[0.62rem] leading-none font-semibold text-slate-600 dark:border-white/15 dark:bg-white/10 dark:text-white/70;
}

inline code 不再抢正文注意力

inline code 这次改成了低对比度、有边界的 token。它仍然能被识别为代码,但不会像按钮一样跳出来。

css/tailwind.css在 GitHub 查看代码
    & :where(code):not(pre code) {
      border: 1px solid color-mix(in oklab, var(--color-slate-300) 72%, transparent);
      border-radius: 6px;
      background: linear-gradient(
        to bottom,
        color-mix(in oklab, white 92%, var(--color-slate-100)),
        color-mix(in oklab, var(--color-slate-100) 88%, white)
      );
      box-shadow: inset 0 -1px 0 color-mix(in oklab, var(--color-slate-300) 45%, transparent);
      color: #4c4f69;
      padding: 0.08rem 0.34rem;
      font-size: 0.86em;
      font-weight: 500;
      line-height: 1.75;
      word-break: break-word;

GitHub 代码和 diff 仍然走 Shiki

::github-code::github-diff 在构建时会拉取真实代码或 diff,再生成普通 code node。也就是说它们不需要特殊渲染器,最后仍然交给 Shiki 高亮。

contentlayer.config.ts在 GitHub 查看代码
    if (kind === 'code') {
      const value = selectCodeLines(await getGitHubFile(attrs), attrs.lines)
      const title =
        attrs.title ||
        `${normalizeGitHubRepo(attrs.repo)}:${attrs.path}${attrs.lines ? `#L${attrs.lines}` : ''}`
 
      return createCodeNode(value, attrs.lang || 'text', title, {
        showLineNumbers: true,
        sourceUrl: getGitHubCodeUrl(attrs),
      })

diff 的路径也一样,只是数据源换成 patch,语言固定成 diff

contentlayer.config.ts在 GitHub 查看代码
    const value = selectCodeLines(await getGitHubDiff(attrs), attrs.lines)
    const title =
      attrs.title ||
      `${normalizeGitHubRepo(attrs.repo)}:${attrs.path || `${attrs.base || attrs.ref}...${attrs.head || ''}`}`
 
    return createCodeNode(value || 'No diff matched this query.', attrs.lang || 'diff', title, {
      showLineNumbers: true,
      sourceUrl: getGitHubDiffUrl(attrs),
    })

这也是为什么 GitHub 引用块能同时拥有:Shiki 颜色主题、语言图标、复制按钮、GitHub 跳转和 diff 背景。

现在写博客的心智模型

普通文章只管写 Markdown。需要图标时写 :icon-code:,需要行情时写 $AAPL::tv AAPL,需要引用源码时写 ::github-code,需要引用改动时写 ::github-diff

构建阶段会把这些短语法转换成稳定的 MDX 组件或 Shiki 代码块;运行时只负责交互和外部 widget 加载。这样写作语法短,页面输出也可控。

除另有说明,本文内容采用 CC BY-NC-SA 4.0 协议许可。转载或改编请署名、非商业使用,并以相同方式共享。

Blog收起文章