前言
在我编写的在线字典网站中,我碰到了如下一个需求,如下图所示:
让我为你叙述这个需求需要做什么。
需求分析
在 Markdown 中,我们希望能够渲染一个特定的字符(如汉字),并且它会被一个圆圈包围。为了满足这一需求,我们需要做的工作是:
- 使用正则表达式匹配 Markdown 中的特定符号(如:
o汉字o
)。 - 将匹配到的字符替换为 HTML 元素(如:
<span class="char-circle">汉字</span>
)。 - 使用 CSS 为这个 HTML 元素添加样式,使得字符被圆圈包围。
对于这个需求,我们只需要扩展一下markdown语法即可,我们先来看看需求的实现。
实现思路
- 扩展 Markdown 配置:
我们可以通过修改vitepress
的 Markdown 配置来实现这一功能。在vitepress
中,我们可以使用config
函数来扩展 Markdown 渲染规则,修改默认的文本渲染方式。 - CSS 样式:
通过为匹配到的字符添加一个span
标签,并为这个标签设置圆形边框和其他样式,实现字符的圆圈效果。 - 使用正则表达式:
我们通过正则表达式来匹配包裹在o
字符中的汉字,并将其转换为带有样式的 HTML 标签。
具体实现
如何扩展markdown配置
查阅官方文档,我们发现对于扩展markdown的描述很简单,只是提供了一个markdown属性,而我们就需要根据这个属性来做文章。
根据ts定义文件,这个属性提供了很多配置属性,接口定义如下:
ts
interface Options$2 {
html?: boolean | undefined;
xhtmlOut?: boolean | undefined;
breaks?: boolean | undefined;
langPrefix?: string | undefined;
linkify?: boolean | undefined;
typographer?: boolean | undefined;
quotes?: string | string[];
highlight?: ((str: string, lang: string, attrs: string) => string) | null | undefined;
}
interface MarkdownOptions extends Options$2 {
preConfig?: (md: MarkdownIt) => void;
config?: (md: MarkdownIt) => void;
cache?: boolean;
externalLinks?: Record<string, string>;
theme?: ThemeOptions;
languages?: LanguageInput[];
languageAlias?: Record<string, string>;
lineNumbers?: boolean;
defaultHighlightLang?: string;
codeTransformers?: ShikiTransformer[];
shikiSetup?: (shiki: Highlighter) => void | Promise<void>;
codeCopyButtonTitle?: string;
anchor?: anchor.AnchorOptions;
attrs?: {
leftDelimiter?: string;
rightDelimiter?: string;
allowedAttributes?: Array<string | RegExp>;
disable?: boolean;
};
emoji?: {
defs?: Record<string, string>;
enabled?: string[];
shortcuts?: Record<string, string | string[]>;
};
frontmatter?: FrontmatterPluginOptions;
headers?: HeadersPluginOptions | boolean;
sfc?: SfcPluginOptions;
toc?: TocPluginOptions;
component?: ComponentPluginOptions;
container?: ContainerOptions;
math?: boolean | any;
image?: Options$1;
gfmAlerts?: boolean;
}
如果将每一个属性都解释一下,那就可以另外写一篇文章了,所以这里暂时打住,这里我们需要用到的是config属性,它是一个函数,函数的第一个参数是一个md实例。由于vitepress底层采用的是markdown-it插件,因此想要了解md实例的具体属性,也需要我们去查看这个插件的文档了解api属性,这同样不在本文的范畴,因此这里只是固定告诉大家,我们需要用到md.renderer.rules.text
属性,该属性接受一个返回格式化后的文本字符串。
相信读者能够猜到怎么回事了,对,没错,我将使用正则表达式来匹配这个字符,然后为这个字符转换成一个带有样式的html字符串,然后返回即可。代码结构如下:
ts
// .vitepress/config.mts中
import { defineConfig } from 'vitepress'
export default defineConfig({
markdown: {
config: (md) => {
md.renderer.rules.text = (tokens, idx) => {
const text = tokens[idx].content;
// 核心代码
return text;
};
},
},
})
css样式的实现
通过上图所示,接下来我们需要实现一个带圆圈的汉字,这很简单,我们利用边框属性和圆角属性调整一下这个元素即可,具体说来就是,我们将这个字用一个html标签包住,然后给这个标签添加一个class类名,再调整样式即可。html代码和css代码分别如下:
html
<span class="char-circle">又</span>
css
.char-circle {
display: inline-block;
width: 18px;
height: 18px;
text-align: center;
line-height: 14px;
font-size: 12px;
padding: 1px;
border-radius: 50%;
border: 1px solid #000000;
vertical-align: text-top;
font-weight: bold;
}
解释一下如上css代码含义:
display: inline-block;
:使得span
标签在同一行显示,并允许设置宽度和高度。width
和height
:设置圆圈的宽度和高度。text-align: center;
和line-height: 14px;
:使得字符在圆圈内居中显示。font-size
:设置字体大小。border-radius: 50%;
:设置圆角效果,使得span
标签变成圆形。border
:设置圆圈的边框。vertical-align: text-top;
:调整文本的垂直位置,使其与其他文本对齐。
这里也许有人好奇,为什么我不直接在每个md文档里面直接写这个html标签元素即可,一篇md文档就不止一个汉字,基本上每个汉字都会有1到2个这种特殊的字符,难道我每一篇文档每一个字的特殊字符都要去写html标签吗?这显然是不合适的,因此我们就需要定义一个规则,然后通过正则表达式来匹配,当满足这个匹配条件,就将这个结果给替换。
正则表达式的实现
这里我使用的是2个o字符来包裹它,如下所示:
md
// md文档
o又o。
当匹配到如上的字符文档,我们就需要将这个字符转换成如下所示的html标签:
html
<span class="char-circle">又</span>
接下来就是如何定义这个正则表达式了,第一步我们可以使用如下的正则表达式:
ts
const rule = /o(.*?)o/g;
解释一下这个正则表达式,其实主要核心在于(.*?)
,这是一个捕获组,其中.*?
表示匹配任意字符。
这个正则表达式能处理大多数情况,但有一种特殊情况不行,例如如下结构的文档:
md
诰(gao):xxxx。o喻o: xxxx。
这里会将gao拼音中的o字母作为开始字符,然后喻前面的o字符作为结束字符匹配,这显然不符合我们想要的结果,也就是只匹配o喻o
这三个字符。
让我们更精细的优化一下这个正则表达式,也就是说我们要规定两个o字符之间匹配的一定是一个中文字符,因为只有类似又,喻,特
这样的字符才会是这种特殊字符。那么匹配中文字符的正则表达式是什么?很显然我们需要用到unicode编码,代码如下所示:
ts
[\u4e00-\u9fa5]
调整过后的正则表达式就变成了如下这样:
ts
const rule = /o([\u4e00-\u9fa5])o/g;
注意这里一定有加上()
表示这是一个捕获组,这样方便我们通过捕获组的属性来访问这个匹配到的字符。
接下来我们就需要使用字符串的replace方法来匹配字符串,然后将匹配到的字符转换成我们需要渲染的html标签即可,代码如下所示:
ts
const transformedText = text.replace(/o([\u4e00-\u9fa5])o/g, (_match, p1) => `<span class="char-circle">${p1}</span>`);
可以看到,这里我们通过了第二个参数,也就是捕获组1,来访问匹配到的字符,这就是添加()
的意义所在,至于_match
,就是一个忽略的参数,_
可以理解为占位符,表示这个参数必须要存在,但是不会被后续用到。最后,我们完善一下config函数,如下所示:
ts
// .vitepress/config.mts中
import { defineConfig } from 'vitepress'
export default defineConfig({
markdown: {
config: (md) => {
md.renderer.rules.text = (tokens, idx) => {
const text = tokens[idx].content;
const transformedText = text.replace(/o([\u4e00-\u9fa5])o/g, (_match, p1) => `<span class="char-circle">${p1}</span>`);
return transformedText;
};
},
},
})
如此一来,我们就完成了这个需求。
总结
通过以上步骤,我们实现了在 Markdown 中渲染带圆圈的汉字字符。这个方案非常灵活,因为它能够根据需求自动处理不同的文本,而不需要在每个 Markdown 文档中手动插入 HTML 标签。通过扩展 Markdown 渲染规则和使用正则表达式,我们为用户提供了更加简洁的写作体验。