diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 46d8e6f30..c715a9f9f 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -74,7 +74,13 @@ function sideGuide(): DefaultTheme.SidebarItem[] {
items: [
{ text: 'API操作', link: 'api' },
]
- },
+ }, {
+ text: '高级功能',
+ base: '/cherry/advanced/',
+ items: [
+ { text: '自定义语法', link: 'custom-render' },
+ ]
+ }
]
}
diff --git a/docs/cherry/advanced/custom-render.md b/docs/cherry/advanced/custom-render.md
new file mode 100644
index 000000000..e54fb8ba1
--- /dev/null
+++ b/docs/cherry/advanced/custom-render.md
@@ -0,0 +1,403 @@
+# 自定义语法
+
+ 当已有的功能不能满足你需求的时候,那么你肯定想自己来实现解析markdown字符串进行自定义渲染。
+
+## Cherry.createSyntaxHook(hookName, type, options)
+
+- hookName: `string` 语法名
+- type: `string` 语法类型
+- `Cherry.constants.HOOKS_TYPE_LIST.SEN` 行内语法。
+- `Cherry.constants.HOOKS_TYPE_LIST.PAR` 行内语法。
+- options: `object` 自定义语法的主体逻辑。
+
+## 简单例子
+
+ 自定义一个语法,识别形如 `***ABC***` 的内容,并将其替换成: `ABC`。
+
+```ts
+let CustomHookA = Cherry.createSyntaxHook('important',
+ Cherry.constants.HOOKS_TYPE_LIST.SEN, {
+ makeHtml(str) {
+ return str.replace(this.RULE.reg, function(whole, m1, m2) {
+ return `${m2}`;
+ });
+ },
+ rule(str) {
+ return { reg: /(\*\*\*)([^\*]+)\1/g };
+ },
+});
+```
+
+### 将这个语法配置到cherry配置中
+
+```ts
+new Cherry({
+ id: 'markdow-container',
+ value: '## hello world',
+ fileUpload: myFileUpload,
+ customSyntax: {
+ importHook: {
+ syntaxClass: CustomHookA, // 将自定义语法对象挂载到 importHook.syntaxClass上 //[!code ++]
+ force: false, // true: 当cherry自带的语法中也有一个“importHook”时,用自定义的语法覆盖默认语法; false:不覆盖
+ before: 'fontEmphasis', // 定义该自定义语法的执行顺序,当前例子表明在加粗/斜体语法前执行该自定义语法
+ },
+ },
+ toolbars: {
+ ...
+ },
+});
+```
+
+#### 效果如下图
+
+![custom-render-1](/cherry/advanced/custom-render-1.png)
+
+## 详细解析
+
+### 原理
+
+一言以蔽之,cherry的语法解析引擎就是将一堆正则按一定顺序依次执行,将markdown字符串替换为html字符串的工具。
+
+### 语法分两类
+
+- 行内语法: 即类似加粗、斜体、上下标等主要对文字样式进行控制的语法,最大的特点是可以相互嵌套。
+- 段落语法,即类似表格、代码块、列表等主要对整段文本样式进行控制的语法。
+ 有两个特点:
+ 1. **可以在内部执行行内语法**
+ 2. **可以声明与其他段落语法互斥**
+
+### 语法的组成
+
+```ts
+type createSyntaxHook={
+ name?:string;
+ beforeMakeHtml?:(str:string)=>any;
+ makeHtml?:(str:string)=>any;
+ afterMakeHtml?:(str:string)=>any;
+ rule?:(str:string)=>{reg:Reg};
+ needCache?:boolean;
+}
+```
+
+1. `name`: 唯一的作用就是用来定义语法的执行顺序的时候按语法名排序。
+2. `beforeMakeHtml()`: engine会最先按**语法正序**依次调用`beforeMakeHtml()`。
+3. `makeHtml()`: engine会在调用完所有语法的`beforeMakeHtml()`后,再按**语法正序**依次调用`makeHtml()`。
+4. `afterMakeHtml()`: engine会在调用完所有语法的`makeHtml()`后,再按**语法逆序**依次调用`afterMakeHtml()`。
+5. `rule()`,用来定义语法的正则。
+6. `needCache`: 用来声明是否需要“缓存”,只有段落语法支持这个变量,`true`:段落语法可以在`beforeMakeHtml()`、`makeHtml()`的时候利用`this.pushCache()`和`this.popCache()`实现排它的能力。
+
+### 自带的语法
+
+
+
+#### 行内Hook
+
+> 引擎会按当前顺序执行makeHtml()方法
+
+- emoji 表情
+- image 图片
+- link 超链接
+- autoLink 自动超链接(自动将符合超链接格式的字符串转换成``标签)
+- fontEmphasis 加粗和斜体
+- bgColor 字体背景色
+- fontColor 字体颜色
+- fontSize 字体大小
+- sub 下标
+- sup 上标
+- ruby 一种表明上下排列的排版方式,典型应用就是文字上面加拼音
+- strikethrough 删除线
+- underline 下划线
+- highLight 高亮(就是在文字外层包一个``标签)
+
+#### 段落级 Hook
+
+> 引擎会按当前排序顺序执行beforeMake()、makeHtml()方法
+> 引擎会按当前排序逆序执行afterMake()方法'
+
+- codeBlock 代码块
+- inlineCode 行内代码(因要实现排它特性,所以归类为段落语法)
+- mathBlock 块级公式
+- inlineMath 行内公式(理由同行内代码)
+- htmlBlock html标签,主要作用为过滤白名单外的html标签
+- footnote 脚注
+- commentReference 超链接引用
+- br 换行
+- table 表格
+- blockquote 引用
+- toc 目录
+- header 标题
+- hr 分割线
+- list有序列表、无序列表、checklist
+- detail 手风琴
+- panel 信息面板
+- normalParagraph 普通段落
+
+### 具体介绍
+
+- 如果要实现一个**行内语法**,只需要了解以下三个概念
+ 1. 定义正则 `rule()`
+ 2. 定义具体的正则替换逻辑 `makeHtml()`
+ 3. 确定自定义语法名,并确定执行顺序
+
+- 如果要实现一个**段落语法**,则需要在了解行内语法相关概念后再了解以下概念:
+ 1. 排它机制
+ 2. 局部渲染机制
+ 3. 编辑区和预览区同步滚动机制
+
+## 一例胜千言
+
+### 最简单段落语法
+
+```ts
+/**
+ * 把 \n++\n XXX \n++\n 渲染成 XXX
+ */
+var myBlockHook = Cherry.createSyntaxHook('myBlock', Cherry.constants.HOOKS_TYPE_LIST.PAR, {
+ makeHtml(str) {
+ return str.replace(this.RULE.reg, function(whole, m1) {
+ return `${m1}
`;
+ });
+ },
+ rule(str) {
+ return { reg: /\n\+\+(\n[\s\S]+?\n)\+\+\n/g };
+ },
+});
+...
+new Cherry({
+ ...
+ customSyntax: {
+ myBlock: {
+ syntaxClass: myBlockHook,
+ before: 'blockquote',
+ },
+ },
+ ...
+});
+```
+
+#### 效果如下
+
+![custom-render-2](/cherry/advanced/custom-render-2.png)
+
+#### 遇到问题1
+
+当我们尝试进行段逻语法嵌套时,就会发现这样的问题:
+
+- 问题1:代码块没有放进新语法里。
+- 问题2:产生了一个多余的P标签。
+
+![custom-render-3](/cherry/advanced/custom-render-3.png)
+
+## 理解排它机制
+
+为什么会有这样的问题,则需要先理解cherry的排他机制一言以蔽之,排他就是某个语法利用自己的“先发优势(如`beforeMakeHtml()`、`makeHtml()`)”把符合自己语法规则的内容先替换成**占位符**,
+再利用自己的“后发优势(`afterMakeHtml()`)”将占位符**替换回**html内容。
+
+#### 分析原因1
+
+接下来解释上面出现的"bug"的原因:
+
+1. 新语法(myBlockHook)并没有实现排他操作.
+2. 在 **1** 的前提下,引擎先执行`codeBlock.makeHtml()`,再执行`myBlockHook.makeHtml()`,最后执行`normalParagraph.makeHtml()`(当然还执行了其他语法hook)。
+
+ (**1**). 在执行`codeBlock.makeHtml()`后,源md内容变成了:
+ ![custom-render-4](/cherry/advanced/custom-render-4.png)
+
+ (**2**). 在执行`myBlockHook.makeHtml()`后,原内容变成了:
+ ![custom-render-5](/cherry/advanced/custom-render-5.png)
+
+ (**3**). 在执行`normalParagraph.makeHtml()`时,必须要先讲一下`normalParagraph.makeHtml()`的逻辑了,逻辑如下:
+ 1. normalParagraph认为任意两个同层级排他段落语法之间是**无关的**,所以其会按**段落语法占位符**分割文档,把文档分成若干段落,在本例子中,其把文章内容分成了 `src`、`~CodeBlock的占位符~`、``三块内容,至于为什么这么做,则涉及到了**局部渲染机制**,后续会介绍。
+ 2. normalParagraph在渲染各块内容时会利用[dompurify](https://github.com/cure53/DOMPurify)对内容进行html标签合法性处理,比如会检测到第一段内容div标签没有闭合,会将第一段内容变成`src`这就出现了问题(**1**),然后会判定第三段内容非法,直接把``删掉,这就出现了问题(**2**)。
+
+#### 解决问题1
+
+ 如何解决上述“bug”呢,很简单,只要给myBlockHook实现排他就好了,实现代码如下:
+
+```ts
+/**
+ * 把 \n++\n XXX \n++\n 渲染成 XXX
+ */
+var myBlockHook = Cherry.createSyntaxHook('myBlock', Cherry.constants.HOOKS_TYPE_LIST.PAR, {
+ needCache: true, // 表明要使用缓存,也是实现排他的必要操作
+ makeHtml(str) {
+ const that = this;
+ return str.replace(this.RULE.reg, function(whole, m1) {
+ const result = `\n${m1}
\n`;
+ return that.pushCache(result); // 将结果转成占位符
+ });
+ },
+ rule(str) {
+ return { reg: /\n\+\+(\n[\s\S]+?\n)\+\+\n/g };
+ },
+});
+```
+
+#### 效果如下
+
+![custom-render-6](/cherry/advanced/custom-render-6.png)
+
+#### 遇到问题2
+
+自定义语法没有被渲染出来。
+
+## 理解局部渲染机制
+
+ 为什么自定义语法没有渲染出来,则需要了解cherry的局部渲染机制,首先看以下例子:
+
+ ![custom-render-7](/cherry/advanced/custom-render-7.png)
+
+ > 局部渲染的目的就是为了在文本量很大时提高性能,做的无非两件事:减少每个语法的执行次数(局部解析),减少预览区域dom的变更范围(局部渲染)。
+
+ 局部解析的机制与当前问题无关先按下不表,接下来解释局部渲染的实现机制:
+
+ 1. 段落语法根据md内容生成对应html内容时,会提取md内容的特征(md5),并将特征值放进段落标签的`data-sign`属性中
+ 2. 预览区会将已有段落的`data-sign`和引擎生成的新段落`data-sign`进行对比
+ - 如果`data-sign`值没有变化,则认为当前段落内容没有变化
+ - 如果段落内容有变化,则用新段落替换旧段落
+
+#### 分析原因2
+
+接下来解释上面出现的“bug”的原因:
+
+1. 新语法(`myBlockHook`)输出的html标签里并没有`data-sign`属性
+2. 预览区在拿到新的html内容时,会获取有`data-sign`属性的段落,并将其更新到预览区
+3. 由于`myBlockHook`没有`data-sign`属性,但`codeBlock`有`data-sign`属性,所以只有代码块被渲染到了预览区
+
+#### 解决问题2
+
+如何解决上述“bug”呢,很简单,只要给myBlockHook实现排他就好了,实现代码如下:
+
+```ts
+/**
+ * 把 \n++\n XXX \n++\n 渲染成 XXX
+ */
+var myBlockHook = Cherry.createSyntaxHook('myBlock', Cherry.constants.HOOKS_TYPE_LIST.PAR, {
+ needCache: true, // 表明要使用缓存,也是实现排他的必要操作
+ makeHtml(str) {
+ const that = this;
+ return str.replace(this.RULE.reg, function(whole, m1) {
+ const sign = that.$engine.md5(whole); // 定义sign,用来实现局部渲染
+ const lines = that.getLineCount(whole); // 定义行号,用来实现联动滚动
+ const result = `\n${m1}
\n`;
+ return that.pushCache(result, sign, lines); // 将结果转成占位符
+ });
+ },
+ rule(str) {
+ return { reg: /\n\+\+(\n[\s\S]+?\n)\+\+\n/g };
+ },
+});
+```
+
+> 注:data-lines属性是用来实现编辑区和预览区联动滚动的
+
+#### 效果如下
+
+![custom-render-8](/cherry/advanced/custom-render-8.png)
+
+#### 遇到问题3
+
+新段落语法里的行内语法没有被渲染出来。
+
+#### 解决问题3
+
+段落语法的`makeHtml()`会传入**两个**参数(行内语法的只会传入**一个**参数),第二个参数是`sentenceMakeFunc`(行内语法渲染器)
+
+```ts
+/**
+ * 把 \n++\n XXX \n++\n 渲染成 XXX
+ */
+var myBlockHook = Cherry.createSyntaxHook('myBlock', Cherry.constants.HOOKS_TYPE_LIST.PAR, {
+ needCache: true, // 表明要使用缓存,也是实现排他的必要操作
+ makeHtml(str, sentenceMakeFunc) {
+ const that = this;
+ return str.replace(this.RULE.reg, function(whole, m1) {
+ // const sign = that.$engine.md5(whole); // 定义sign,用来实现局部渲染
+ const lines = that.getLineCount(whole); // 定义行号,用来实现联动滚动
+ const { sign, html } = sentenceMakeFunc(m1); // 解析行内语法
+ const result = `\n${html}
\n`;
+ return that.pushCache(result, sign, lines); // 将结果转成占位符
+ });
+ },
+ rule(str) {
+ return { reg: /\n\+\+(\n[\s\S]+?\n)\+\+\n/g };
+ },
+});
+```
+
+## 总结
+
+- 如果要实现一个**行内语法**,只需要实现以下三个功能
+ 1. 定义正则 rule()
+ 2. 定义具体的正则替换逻辑 makeHtml()
+ 3. 确定自定义语法名,并确定执行顺序
+- 如果要实现一个**段落语法**,则需要在实现上面三个功能后,同时实现以下三个功能
+ 1. 排它机制 `needCache: true`
+ 2. 局部渲染机制 `data-sign`
+ 3. 编辑区和预览区同步滚动机制 `data-lines`
+
+由于上面已有自定义行内语法的实现例子,接下来我们将通过实现一个**自定义段落语法**的例子来了解各个机制。
+
+### 完整例子
+
+```ts
+/**
+ * 自定义一个语法,识别形如 ***ABC*** 的内容,并将其替换成 ABC
+ */
+var CustomHookA = Cherry.createSyntaxHook('important', Cherry.constants.HOOKS_TYPE_LIST.SEN, {
+ makeHtml(str) {
+ return str.replace(this.RULE.reg, function(whole, m1, m2) {
+ return `${m2}`;
+ });
+ },
+ rule(str) {
+ return { reg: /(\*\*\*)([^\*]+)\1/g };
+ },
+});
+
+/**
+ * 把 \n++\n XXX \n++\n 渲染成 XXX
+ */
+var myBlockHook = Cherry.createSyntaxHook('myBlock', Cherry.constants.HOOKS_TYPE_LIST.PAR, {
+ needCache: true, // 表明要使用缓存,也是实现排他的必要操作
+ makeHtml(str, sentenceMakeFunc) {
+ const that = this;
+ return str.replace(this.RULE.reg, function(whole, m1) {
+ // const sign = that.$engine.md5(whole); // 定义sign,用来实现局部渲染
+ const lines = that.getLineCount(whole); // 定义行号,用来实现联动滚动
+ const { sign, html } = sentenceMakeFunc(m1); // 解析行内语法
+ const result = `\n${html}
\n`;
+ return that.pushCache(result, sign, lines); // 将结果转成占位符
+ });
+ },
+ rule(str) {
+ return { reg: /\n\+\+(\n[\s\S]+?\n)\+\+\n/g };
+ },
+});
+
+new Cherry({
+ id: 'markdown-container',
+ value: '## hello world',
+ fileUpload: myFileUpload,
+ customSyntax: {
+ importHook: {
+ syntaxClass: CustomHookA, // 将自定义语法对象挂载到 importHook.syntaxClass上
+ force: false, // true: 当cherry自带的语法中也有一个“importHook”时,用自定义的语法覆盖默认语法; false:不覆盖
+ before: 'fontEmphasis', // 定义该自定义语法的执行顺序,当前例子表明在加粗/斜体语法前执行该自定义语法
+ },
+ myBlock: {
+ syntaxClass: myBlockHook,
+ force: true,
+ before: 'blockquote',
+ },
+ },
+ toolbars: {
+ ...
+ },
+});
+
+```
+
+### 最终效果如下
+
+![custom-render-9](/cherry/advanced/custom-render-9.png)
diff --git a/docs/public/cherry/advanced/custom-render-1.png b/docs/public/cherry/advanced/custom-render-1.png
new file mode 100644
index 000000000..f33a5460e
Binary files /dev/null and b/docs/public/cherry/advanced/custom-render-1.png differ
diff --git a/docs/public/cherry/advanced/custom-render-2.png b/docs/public/cherry/advanced/custom-render-2.png
new file mode 100644
index 000000000..535fbaf61
Binary files /dev/null and b/docs/public/cherry/advanced/custom-render-2.png differ
diff --git a/docs/public/cherry/advanced/custom-render-3.png b/docs/public/cherry/advanced/custom-render-3.png
new file mode 100644
index 000000000..b9040c72d
Binary files /dev/null and b/docs/public/cherry/advanced/custom-render-3.png differ
diff --git a/docs/public/cherry/advanced/custom-render-4.png b/docs/public/cherry/advanced/custom-render-4.png
new file mode 100644
index 000000000..8e530640f
Binary files /dev/null and b/docs/public/cherry/advanced/custom-render-4.png differ
diff --git a/docs/public/cherry/advanced/custom-render-5.png b/docs/public/cherry/advanced/custom-render-5.png
new file mode 100644
index 000000000..dabaf21be
Binary files /dev/null and b/docs/public/cherry/advanced/custom-render-5.png differ
diff --git a/docs/public/cherry/advanced/custom-render-6.png b/docs/public/cherry/advanced/custom-render-6.png
new file mode 100644
index 000000000..885340e78
Binary files /dev/null and b/docs/public/cherry/advanced/custom-render-6.png differ
diff --git a/docs/public/cherry/advanced/custom-render-7.png b/docs/public/cherry/advanced/custom-render-7.png
new file mode 100644
index 000000000..b9040c72d
Binary files /dev/null and b/docs/public/cherry/advanced/custom-render-7.png differ
diff --git a/docs/public/cherry/advanced/custom-render-8.png b/docs/public/cherry/advanced/custom-render-8.png
new file mode 100644
index 000000000..dca2de4c4
Binary files /dev/null and b/docs/public/cherry/advanced/custom-render-8.png differ
diff --git a/docs/public/cherry/advanced/custom-render-9.png b/docs/public/cherry/advanced/custom-render-9.png
new file mode 100644
index 000000000..da957e985
Binary files /dev/null and b/docs/public/cherry/advanced/custom-render-9.png differ