react-markdown 扩展规则

2021-12-28 11:18:15 浏览数 (1)

上帝说,要有光,于是便有了光

为了 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}

0 人点赞