写技术文档的时候,我经常遇到一个问题:Markdown 本身虽然简洁,但在展示复杂内容时就有些力不从心。尤其是想插入一些结构化的内容,比如提示框、图文卡片或者是横向图集,传统的 Markdown 写法总显得局促,甚至需要引入 HTML 标签来“打补丁”。这让我开始思考:能不能在不改变 Markdown 核心语法习惯的前提下,让它具备更现代的展示能力?
于是我动手做了一个试验性的项目,一个带有自定义块语法的 Markdown 渲染器。它的目标是:既保留 Markdown 的书写体验,又能通过语法扩展实现现代化的 UI 表现。我想通过这篇文章,记录这个项目从构思到实现的整个过程。
最初的起因其实很简单:我需要一个可以在博客或项目文档中,插入「漂亮又实用」的组件,比如:
而这些,如果单靠 Markdown 原始语法来实现,往往要么写不出来,要么写出来很丑。市面上的 Markdown 渲染器不少,比如 VitePress、Typora、Notion 等,但要么不支持自定义语法,要么需要配合复杂的插件系统。
我想要的,是一种更轻量、可控、原生 Markdown 写法友好的方案。
为了方便修改以及兼容微信公众号,我使用了这个项目 https://github.com/doocs/md 。很 nice 的项目,大家快去 fork。
我不打算破坏 Markdown 原有的纯文本特性,因此,我最终选择了一种基于 :::
的块级标记形式,类似这样:
::: info-box
type=warning
icon=true
background=#fcf8e3
border-left=4px solid #f0ad4e
::style::
注意事项
::head::
请务必在操作前备份数据
:::
它有三个关键部分:
:::
:声明一个自定义块类型(如 info-box
)。::style::
::head::
::split::
等标记划分内容结构。这种语法相比 HTML 更易读,也更方便用正则解析。在渲染前,我通过一个正则表达式将其完整提取出来,再交给对应的组件进行处理。
样式定义部分支持简洁的 key=value
写法,我设计了一个轻量的解析函数:
function parseStyleDefinitions(styleText) {
const styles = {};
const lines = styleText.split('\n').filter(Boolean);
lines.forEach(line => {
const [key, value] = line.split('=').map(part => part.trim());
if (key && value) styles[key] = value;
});
return styles;
}
这个函数的目标很清晰:
所有样式我都使用内联写法,避免全局污染。虽然看起来会稍长,但这带来了高度的隔离性——每一个组件只控制自己的样式,和外界无关,尤其适合嵌入在第三方平台或者静态页面中。
在 Markdown 渲染的过程中,保持结构清晰是一件非常重要的事情。我希望每种自定义块都可以有自己独立的“生命周期”:从解析、到样式处理、再到最终 HTML 输出,都由专属模块完成。这样做最大的好处是:每新增一个新类型的块,不需要动原有逻辑,保持系统的解耦和可维护。
于是我采用了「渲染器组件」的设计思路。每个组件负责一种块类型的渲染,比如:
InfoBoxRenderer
→ 渲染提示框ImageGalleryRenderer
→ 渲染图集块StructuredCardRenderer
→ 渲染特性卡片块每个渲染器接收结构化数据(head/headStyle/splits/styles 等),输出最终 HTML。
这里我用“横向图集”这个模板来举例,讲讲整个过程。
首先 Markdown 中用户会写一段这样的内容:
::: image-gallery-horizontal
gap=16px
padding=24px
radius=12px
shadow=0 4px 12px rgba(0,0,0,0.15)
max-height=280px
::style::
我的摄影作品集
::head::
这是我最近拍摄的一些风景照片
::split::
https://example.com/photo1.jpg
::split::
https://example.com/photo2.jpg
::split::
https://example.com/photo3.jpg
:::
结构上很清晰:
gap
、padding
、shadow
等;::style::
;::head::
;::split::
一行一张。然后我在 ImageGalleryRenderer
中实现 HTML 构建方法:
function buildImageGalleryHorizontalHtml(head, splits, styles) {
const galleryStyle = {
display: 'flex',
overflowX: 'auto',
scrollSnapType: 'x mandatory',
scrollBehavior: 'smooth',
gap: styles.gap || '16px',
padding: styles.padding || '16px'
};
const imgStyle = {
flex: '0 0 auto',
maxHeight: styles['max-height'] || '280px',
borderRadius: styles.radius || '8px',
boxShadow: styles.shadow || '0 2px 8px rgba(0,0,0,0.1)',
objectFit: 'cover'
};
return `
<section>
${head ? `<h3>${head}</h3>` : ''}
<div style="${styleObjectToCss(galleryStyle)}">
${splits.map(url => `
<img
src="${url.trim()}"
loading="lazy"
style="${styleObjectToCss(imgStyle)}"
>
`).join('')}
</div>
</section>
`;
}
整个过程有几个关键点:
flex
和 scroll-snap-type
实现横向滚动;loading=lazy
,提升性能;radius
、shadow
等样式可通过用户设置动态应用;styleObjectToCss()
工具函数生成内联 CSS 字符串,确保样式隔离。用户写的是纯 Markdown,生成的是带完整样式的图集 UI,这种体验我自己在写博客时觉得非常顺手。
渲染器里一个常用的小函数是 styleObjectToCss
,它将 JS 对象格式的样式转为内联 CSS 字符串:
function styleObjectToCss(styleObj) {
return Object.entries(styleObj)
.map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v}`)
.join('; ');
}
比如:
{
maxHeight: "280px",
borderRadius: "8px"
}
会变成:
max-height: 280px; border-radius: 8px;
这样我们可以在构建 HTML 的时候不用关心 CSS 写法细节,直接操作 JS 对象即可。
为了让 Markdown 中也能出现像 Landing Page 一样的“特性展示区”,我设计了一个叫 structured-card
的块类型。它的语法类似这样:
::: structured-card
background=#f8f9fa
border-radius=12px
padding=24px
shadow=0 6px 20px rgba(0,0,0,0.1)
::style::
产品特性
::head::
我们的产品具备以下核心优势:
::split::
界面友好、逻辑清晰、零学习成本。
::split::
高度可定制,适应不同团队工作流。
::split::
精简核心逻辑,渲染与交互表现优异。
:::
每个 ::split::
段落渲染为一张卡片,整体可以是网格、也可以是横向滑动。这样,我们只需要专注于内容组织,不用考虑样式。组件内部会自动决定如何排版布局,并生成响应式设计。
我在卡片模块中默认加入了标题加粗、行距控制、边距控制,最大限度保持文本的可读性和信息层次感。
技术文档里经常要写提示,比如“注意事项”、“操作前请备份”、“这是 Beta 版本”这种。我不希望写一堆冗长的 HTML,只想像这样:
::: info-box
type=info
icon=true
background=#e7f3fe
border-left=4px solid #2196f3
::style::
温馨提示
::head::
请使用稳定网络环境完成此次操作
:::
渲染结果是:
type
参数控制(比如 danger、warning、success、info 等)。我把所有 info-box 的样式配置都集中在 defaultInfoBoxStyleMap
中,用户通过 type
选择即可复用。
为了更清晰地让大家理解各部分的关系,我绘制了一张结构图,展示从 Markdown 源文件到最终页面的处理流程:
这张图也揭示了在修改和匹配渲染设计上的理念:解析与渲染分离,每个模块单独负责自己的逻辑,让整个系统既清晰又容易维护。
整个部分没有用到什么高深的前端技术,也没有炫酷动画和复杂状态管理,但它确实给我带来了满足感:
有时候我们太依赖工具,却忽略了语言结构本身的设计也很重要。Markdown 是语言,也是一种接口,我想通过这里,延伸它的语义范围,又不破坏它的核心简洁美。
最后给大家推荐图床三件套 Github + PicGo + jsdelivr,轻松搭建简易可用的图床。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。