
博客新功能:代码图标、源码引用、行情图表 !
展示这次给博客新增的代码块语言图标、inline code 主题、GitHub 源码和 diff 引用,以及 TradingView 行情图表短语法。
这次主要把博客的写作体验继续往前推了一步:代码块更容易扫读,代码引用能直接指向 GitHub,金融类内容可以用很短的语法插入 TradingView 图表。
先看新功能
代码块有语言图标
普通代码块现在也会有标题栏。左侧是语言图标,右侧仍然保留复制按钮;如果代码来自 GitHub,标题栏还会出现 GitHub 跳转入口。
const feature = '语言图标'
const theme = 'catppuccin'
console.log(feature, theme)inline code 重新做了主题
正文里的 git log --follow、$AAPL、::tv AAPL 不再像一块突兀的亮色贴片。明亮模式下它更接近 Catppuccin Latte 的纸面色,黑暗模式下则压进 Mocha 的低亮度底色。
直接引用 GitHub 代码
写作时输入:
::github-code repo="mizorewww/blog" ref="c067042276d4b4b384c66c0c61fcd4f6716eb599" path="contentlayer.config.ts" lines="251-253" lang="ts" title="contentlayer.config.ts"渲染出来就是带 Shiki 高亮、语言图标和 GitHub 链接的代码块:
const githubEmbedPattern = /^::github-(code|diff)\s+(.+)$/
const tradingViewMiniPattern = /^\$([A-Za-z0-9._:-]+)$/
const tradingViewAdvancedPattern = /^::(?:tv|tv-advanced|tradingview)\s+(.+)$/diff 像 GitHub 一样分行染色
写作时输入:
::github-diff repo="mizorewww/blog" ref="c067042276d4b4b384c66c0c61fcd4f6716eb599" path="css/prism.css" lines="1-15"渲染效果:
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
写作时单独一行输入:
$AAPL
$BINANCE:BTCUSDT.P渲染效果:
Advanced Chart 也有短语法
写作时输入:
::tv AAPL interval=60 height=460渲染效果:
代码逻辑
短语法先在 remark 阶段变成组件
这两个正则负责识别行情图表写法。$AAPL 是 Mini Chart,::tv AAPL 是 Advanced Chart。
const githubEmbedPattern = /^::github-(code|diff)\s+(.+)$/
const tradingViewMiniPattern = /^\$([A-Za-z0-9._:-]+)$/
const tradingViewAdvancedPattern = /^::(?:tv|tv-advanced|tradingview)\s+(.+)$/Mini Chart 的转换很克制:只有整段内容就是一个 ticker 时才会替换,避免正文里随手提到 $AAPL 就插入一个大图表。
function createTradingViewMiniNode(symbol: string): MdastNode | null {
if (!isTradingViewTicker(symbol)) {
return null
}
return createMdxFlowNode('TradingViewMiniChart', {
symbol: normalizeTradingViewSymbol(symbol),
})Advanced Chart 多一步解析参数。比如 height=460、interval=60 会作为 JSX attribute 传给组件。
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 编译前,浏览器里不会再扫字符串。
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;主题或参数变化时重新生成。
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,就不会改动。
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,再保证有一个标题栏。
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 时,就回退到语言名,比如 TypeScript、Diff、Markdown。
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 变重。
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。它仍然能被识别为代码,但不会像按钮一样跳出来。
& :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 高亮。
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。
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 协议许可。转载或改编请署名、非商业使用,并以相同方式共享。