上帝说,要有光,于是便有了光
为了 Markdown 更加具有可玩性,一般我们无法满足于标准的 Markdown 语法,所以有了 GFM (GitHub Flavored Markdown),这是 GitHub 扩展 Markdown 语法的规范。但是如果这也无法满足我们的需求呢?那么就需要我们自己来定制了。
开始之前
首先需要安装如下几个库
代码语言:javascript复制1yarn add react-markdown remark-parse
COPY
至于需要 react 之类的话,就不必多说了。此文章基于 react-markdown 库进行定制 markdown 语法。
简单使用
react-markdown 的使用方法非常简单,只需要这样就行了。
tsx
代码语言:javascript复制1import ReactMarkdown, { ReactMarkdownProps } from 'react-markdown';
2const Markdown: FC = props => {
3 return <ReactMarkdown>
4 {/* value 为 markdown 内容 */}
5 {props.value}
6 </ReactMarkdown>
7}
COPY
一般的,到这里其实就 ok 了。
如果你不满足于此,那么进入今天的主题。
定制语法
spoiler 是一个新的语法,token 为 ||这是文字||
,通过两个竖线包裹,被渲染为文字和背景同色,鼠标移上后使背景透明。类似萌娘的剧透内容的样式。
防剧透内容
扩展之前,我们首先要知道 react-markdown 是对 remark 的一次封装,所以是可以使用 remark 的插件来扩展语法的。那么接下来我们就来做一个插件。
tsx
代码语言:javascript复制1// rules/spoiler.ts
2import { Eat, Parser } from 'remark-parse';
3
4function tokenizeSpoiler(eat: Eat, value: string, silent?: boolean): any {
5 const match = /||(.*)||/.exec(value);
6
7 if (match) {
8 if (silent) {
9 return true;
10 }
11 try {
12 return eat(match[0])({
13 type: 'spoiler',
14 value: match[1]
15 });
16 } catch {
17 // console.log(match[0]);
18 }
19 }
20}
21tokenizeSpoiler.notInLink = true;
22tokenizeSpoiler.locator = function(value, fromIndex) {
23 return value.indexOf('||', fromIndex);
24};
25
26function spoilerSyntax(this: any) {
27 const Parser = this.Parser as { prototype: Parser };
28 const tokenizers = Parser.prototype.inlineTokenizers;
29 const methods = Parser.prototype.inlineMethods;
30
31 // Add an inline tokenizer (defined in the following example).
32 tokenizers.spoiler = tokenizeSpoiler;
33
34 // Run it just before `text`.
35 methods.splice(methods.indexOf('text'), 0, 'spoiler');
36}
37export { spoilerSyntax };
38
39// index.tsx
40// import ...
41const RenderSpoiler: FC<{ value: string }> = props => {
42 return <span className={styles['spoiler']}>{props.value}</span>;
43};
44
45const Markdown = props => {
46 return <ReactMarkdown plugins={[spoilerSyntax]}
47 renderers={{
48 spoiler: RenderSpoiler
49 }}
50 >
51 {props.value}
52 </ReactMarkdown>
53}
COPY
以上的代码就完成了一个插件的开发,是不是特别简单呢。
你说你看不懂?没事,慢慢来。
首先,react-markdown 支持传入 plugins,为一个数组。数组里每个元素是一个函数,值得注意的是这个函数中的 this
是有值的,所以不要习惯用箭头函数了。
ts
代码语言:javascript复制1function spoilerSyntax(this: any) { // 插件入口函数
2 const Parser = this.Parser as { prototype: Parser };
3 const tokenizers = Parser.prototype.inlineTokenizers;
4 const methods = Parser.prototype.inlineMethods; // 获取所有的 inline types 的渲染顺序 是一个数组
5
6 tokenizers.spoiler = tokenizeSpoiler; // 把我们定义的渲染器挂载到上面
7 // spoiler 为 name,如果是自定义规则,那么这个 name 和下面的 第三个参数 应相同
8
9 methods.splice(methods.indexOf('text'), 0, 'spoiler'); // 把定义的规则放在哪个顺序执行呢,就放在 `text` 之前吧。`text` 也是一个规则,在整个渲染的最后一个
10
11}
COPY
那么这就是入口函数了,接下来来看 tokenizeSpoiler
函数, 这个是定义如何解析的函数。
ts
代码语言:javascript复制1function tokenizeSpoiler(eat: Eat, value: string, silent?: boolean): any {
2 const match = /||(.*)||/.exec(value); // 通过正则匹配字符串, value 是这一行的字符串
3
4 if (match) {
5 if (silent) { // 这个我也不知道干嘛用的,没用过,可以省略
6 return true;
7 }
8 try { // 多吃可能导致 crash, 需要 catch
9 return eat(match[0])({
10 type: 'spoiler', // 自定义类型,必须在入口函数注册该名称,或使用内置名称
11 value: match[1]
12 });
13 } catch {
14 // console.log(match[0]);
15 }
16 }
17}
18// 内联规格必须制定一个定位器,以保证性能。一般是规则前缀
19tokenizeSpoiler.locator = function(value, fromIndex) {
20 return value.indexOf('||', fromIndex);
21};
COPY
主要说一下 eat
函数,这个名字起得有点奇怪,不过理解之后就感觉起得很生动。
这是一个柯里化 (Currying) 函数,传入一个字符串,一般是匹配到的字符串,返回一个函数,该函数是你对上一个传入的字符串,做何种解析,需要传一个对象。相当于前一个函数是把原字符串(待解析)的传入串吃掉了,后一个就是这么吐出来的过程。除了type
是必须的,其他的任意,你可以传入任意 key-value,都会在渲染的时候暴露出来。
回到 Markdown 组件。
tsx
代码语言:javascript复制1// index.tsx
2// import ...
3import styles from './index.module.scss';
4import ReactMarkdown, { ReactMarkdownProps } from 'react-markdown';
5
6// 这个 value 就是之前 eat 传入的对象中的 value,在这里暴露出来了
7const RenderSpoiler: FC<{ value: string }> = props => {
8 // 可以写点 styles 装饰一下?当然可以!
9 return <span className={styles['spoiler']}>{props.value}</span>;
10};
11
12const Markdown = props => {
13 return <ReactMarkdown plugins={[spoilerSyntax]} // 这个插件就是刚刚写得导出项
14 renderers={{ // 为 spoiler 指定 renderer
15 spoiler: RenderSpoiler
16 }}
17 >
18 {props.value}
19 </ReactMarkdown>
20}
COPY
scss
代码语言:javascript复制1// index.module.scss
2.spoiler {
3 background-color: currentColor;
4 transition: background 0.5s;
5 &:hover {
6 background-color: transparent;
7 }
8 }
COPY
到此为止,一个简单的规则就完成了。是不是很简单呢。
定义多个
很多情况我们不止于只定义单个规则,既然多个,就需要封装。
这里给一个示例代码,之后有时间再详讲。
ts
代码语言:javascript复制1interface defineNewinLineSyntaxProps {
2 regexp: RegExp;
3 type: string;
4 name?: string;
5 locator: string | Locator;
6 render?: ({ value: string }) => JSX.Element | null;
7 handler?: (
8 eat: Eat,
9 type: string,
10 value: string,
11 matched: RegExpExecArray | null
12 ) => object;
13}
14
15export function defineNewinLineSyntax({
16 regexp,
17 type,
18 locator,
19 render,
20 handler,
21 name
22}: defineNewinLineSyntaxProps) {
23 function tokenize(eat: Eat, value: string, silent?: boolean): any {
24 const match = regexp.exec(value);
25
26 if (match) {
27 if (silent) {
28 return true;
29 }
30 try {
31 return (
32 handler?.(eat, type, value, match) ??
33 eat(match[0])({
34 type,
35 value: match[1],
36 component: render?.({ value: match[1] as string }) ?? null
37 })
38 );
39 } catch {
40 // console.log(match[0]);
41 }
42 }
43 }
44 tokenize.notInLink = true;
45 tokenize.locator =
46 typeof locator === 'function'
47 ? locator
48 : function(value, fromIndex) {
49 return value.indexOf(locator, fromIndex);
50 };
51
52 return function(this: any) {
53 const Parser = this.Parser as { prototype: Parser };
54 const tokenizers = Parser.prototype.inlineTokenizers;
55 const methods = Parser.prototype.inlineMethods;
56
57 tokenizers[name ?? type] = tokenize;
58
59 methods.splice(methods.indexOf('text'), 0, name ?? type);
60 };
61}