diff --git a/examples/markdown/basic.md b/examples/markdown/basic.md
index 5a61c6ed9..631d811b0 100644
--- a/examples/markdown/basic.md
+++ b/examples/markdown/basic.md
@@ -310,6 +310,44 @@
:::
+---
+
+## 手风琴
+
+**说明**
+使用连续三个加号`+++`和关键字(`[ + | - ]`)来声明,关键字`+`表示默认收起,关键字`-`表示默认展开
+```
+++++
+点击展开更多
+:
+内容
++-
+默认展开
+:
+内容
+++
+默认收起
+:
+内容
++++
+```
+
+**效果**
+++++
+点击展开更多
+:
+内容
++-
+默认展开
+:
+内容
+++
+默认收起
+:
+内容
++++
+
+
---
## 语法高亮
diff --git a/examples/scripts/index-demo.js b/examples/scripts/index-demo.js
index 6da04b8e6..8d83556d1 100644
--- a/examples/scripts/index-demo.js
+++ b/examples/scripts/index-demo.js
@@ -155,6 +155,7 @@ var basicConfig = {
'ul',
'checklist',
'panel',
+ 'detail',
'|',
'formula',
{
diff --git a/src/Cherry.config.js b/src/Cherry.config.js
index f2873c1b5..fb4a79c49 100644
--- a/src/Cherry.config.js
+++ b/src/Cherry.config.js
@@ -220,6 +220,7 @@ const defaultConfig = {
'|',
'list',
'panel',
+ 'detail',
{
insert: [
'image',
diff --git a/src/Previewer.js b/src/Previewer.js
index 8761086cf..7b5acb62e 100644
--- a/src/Previewer.js
+++ b/src/Previewer.js
@@ -496,11 +496,14 @@ export default class Previewer {
continue;
}
}
- if (/^(class|id|href|rel|target|src|title|controls|align|width|height|style)$/i.test(name)) {
+ if (/^(class|id|href|rel|target|src|title|controls|align|width|height|style|open)$/i.test(name)) {
name = name === 'class' ? 'className' : name;
if (name === 'style') {
ret.style = ret.style ? ret.style : [];
ret.style.push(value);
+ } else if (name === 'open') {
+ // 只要有open这个属性,就一定是true
+ ret[name] = true;
} else {
ret[name] = value;
}
diff --git a/src/core/HooksConfig.js b/src/core/HooksConfig.js
index 32e562b41..3bd2c4184 100644
--- a/src/core/HooksConfig.js
+++ b/src/core/HooksConfig.js
@@ -48,6 +48,7 @@ import HighLight from './hooks/HighLight';
import Suggester from './hooks/Suggester';
import Ruby from './hooks/Ruby';
import Panel from './hooks/Panel';
+import Detail from './hooks/Detail';
/**
* 引擎各语法的配置
* 主要决定支持哪些语法,以及各语法的执行顺序
@@ -71,6 +72,7 @@ const hooksConfig = [
Header, // 处理标题, 传入strict属性严格要求ATX风格标题#后带空格
Hr,
List,
+ Detail,
Panel,
Paragraph, // 普通段落
diff --git a/src/core/hooks/Br.js b/src/core/hooks/Br.js
index 7e2564bf7..54f2198f3 100644
--- a/src/core/hooks/Br.js
+++ b/src/core/hooks/Br.js
@@ -35,7 +35,7 @@ export default class Br extends ParagraphBase {
if (index === 0) {
return match;
}
- const lineCount = lines.match(/\n/g).length;
+ const lineCount = lines.match(/\n/g)?.length ?? 0;
const sign = `br${lineCount}`;
let html = '';
if (isBrowser()) {
@@ -49,7 +49,7 @@ export default class Br extends ParagraphBase {
// node环境下直接输出br
html = this.classicBr ? '' : '
';
}
- const placeHolder = this.pushCache(html, sign);
+ const placeHolder = this.pushCache(html, sign, lineCount);
// 结尾只补充一个\n是因为Br将下一个段落中间的所有换行都替换掉了,而两个换行符会导致下一个区块行数计算错误
return `\n\n${placeHolder}\n`;
});
diff --git a/src/core/hooks/Detail.js b/src/core/hooks/Detail.js
new file mode 100644
index 000000000..4a34e1fce
--- /dev/null
+++ b/src/core/hooks/Detail.js
@@ -0,0 +1,110 @@
+/**
+ * Copyright (C) 2021 THL A29 Limited, a Tencent company.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import ParagraphBase from '@/core/ParagraphBase';
+import { prependLineFeedForParagraph } from '@/utils/lineFeed';
+import { getDetailRule } from '@/utils/regexp';
+import { blockNames } from '@/utils/sanitize';
+
+/**
+ * +++(+|-)
+ * 点击查看详情
+ * :
+ * body
+ * body
+ * +-
+ * 标题(默认收起内容)
+ * :
+ * 内容
+ * ++
+ * 标题(默认展开内容)
+ * :
+ * 内容2
+ * +++
+ */
+export default class Detail extends ParagraphBase {
+ static HOOK_NAME = 'detail';
+
+ constructor() {
+ super({ needCache: true });
+ }
+
+ makeHtml(str, sentenceMakeFunc) {
+ return str.replace(this.RULE.reg, (match, preLines, isShow, content) => {
+ const lineCount = this.getLineCount(match, preLines);
+ const sign = this.$engine.md5(match);
+ const { type, html } = this.$getDetailInfo(isShow, content, sentenceMakeFunc);
+ const ret = this.pushCache(
+ `
${html}
`,
+ sign,
+ lineCount,
+ );
+ return prependLineFeedForParagraph(match, ret);
+ });
+ }
+
+ $getDetailInfo(isShow, str, sentenceMakeFunc) {
+ const type = /\n\s*(\+\+|\+-)\s*\n/.test(str) ? 'multiple' : 'single';
+ const arr = str.split(/\n\s*(\+\+|\+-)\s*\n/);
+ let defaultShow = isShow !== '+';
+ let html = '';
+ if (type === 'multiple') {
+ arr.forEach((item) => {
+ if (/(\+\+|\+-)/.test(item)) {
+ defaultShow = item !== '++';
+ return true;
+ }
+ html += this.$getDetailHtml(defaultShow, item, sentenceMakeFunc);
+ });
+ } else {
+ html = this.$getDetailHtml(defaultShow, str, sentenceMakeFunc);
+ }
+ return { type, html };
+ }
+
+ $getDetailHtml(defaultShow, str, sentenceMakeFunc) {
+ let ret = ``;
+ const paragraphProcessor = (str) => {
+ if (str.trim() === '') {
+ return '';
+ }
+ // 调用行内语法,获得段落的签名和对应html内容
+ const { html } = sentenceMakeFunc(str);
+ let domName = 'p';
+ // 如果包含html块级标签(比如div、blockquote等),则当前段落外层用div包裹,反之用p包裹
+ const isContainBlockTest = new RegExp(`<(${blockNames})[^>]*>`, 'i');
+ if (isContainBlockTest.test(html)) {
+ domName = 'div';
+ }
+ return `<${domName}>${this.$cleanParagraph(html)}${domName}>`;
+ };
+ str.replace(/(^[\w\W]+?)\n\s*:\s*\n([\w\W]+$)/, (match, title, body) => {
+ ret += `${sentenceMakeFunc(title).html}
`;
+ let $body = '';
+ if (this.isContainsCache(body)) {
+ $body = this.makeExcludingCached(body, paragraphProcessor);
+ } else {
+ $body = paragraphProcessor(body);
+ }
+ ret += `${$body}
`;
+ });
+ ret += ` `;
+ return ret;
+ }
+
+ rule() {
+ return getDetailRule();
+ }
+}
diff --git a/src/core/hooks/List.js b/src/core/hooks/List.js
index e7e994860..c93642626 100644
--- a/src/core/hooks/List.js
+++ b/src/core/hooks/List.js
@@ -224,14 +224,23 @@ export default class List extends ParagraphBase {
const text = wholeMatch.replace(/~0$/g, '').replace(/^\n+/, '');
this.buildTree(makeChecklist(text), sentenceMakeFunc);
const result = this.renderTree(0);
- return this.pushCache(result, this.sign);
+ return this.pushCache(result, this.sign, this.$getLineNum(wholeMatch));
+ }
+
+ $getLineNum(str) {
+ const beginLine = str.match(/^\n\n/)?.length ?? 0;
+ const $str = str.replace(/^\n+/, '').replace(/\n+$/, '\n');
+ return $str.match(/\n/g)?.length ?? 0 + beginLine;
}
makeHtml(str, sentenceMakeFunc) {
let $str = `${str}~0`;
if (this.test($str)) {
$str = $str.replace(this.RULE.reg, (wholeMatch) => {
- return this.getCacheWithSpace(this.checkCache(wholeMatch, sentenceMakeFunc), wholeMatch);
+ return this.getCacheWithSpace(
+ this.checkCache(wholeMatch, sentenceMakeFunc, this.$getLineNum(wholeMatch)),
+ wholeMatch,
+ );
});
}
$str = $str.replace(/~0$/g, '');
diff --git a/src/core/hooks/Panel.js b/src/core/hooks/Panel.js
index bb24ff9b0..d9b82e6e6 100644
--- a/src/core/hooks/Panel.js
+++ b/src/core/hooks/Panel.js
@@ -16,6 +16,7 @@
import ParagraphBase from '@/core/ParagraphBase';
import { prependLineFeedForParagraph } from '@/utils/lineFeed';
import { getPanelRule } from '@/utils/regexp';
+import { blockNames } from '@/utils/sanitize';
/**
* 面板语法
* 例:
@@ -60,7 +61,27 @@ export default class Panel extends ParagraphBase {
ret.title = `${
ret.title
}
`;
- ret.body = `${this.$cleanParagraph(sentenceMakeFunc(ret.body).html)}
`;
+ const paragraphProcessor = (str) => {
+ if (str.trim() === '') {
+ return '';
+ }
+ // 调用行内语法,获得段落的签名和对应html内容
+ const { html } = sentenceMakeFunc(str);
+ let domName = 'p';
+ // 如果包含html块级标签(比如div、blockquote等),则当前段落外层用div包裹,反之用p包裹
+ const isContainBlockTest = new RegExp(`<(${blockNames})[^>]*>`, 'i');
+ if (isContainBlockTest.test(html)) {
+ domName = 'div';
+ }
+ return `<${domName}>${this.$cleanParagraph(html)}${domName}>`;
+ };
+ let $body = '';
+ if (this.isContainsCache(ret.body)) {
+ $body = this.makeExcludingCached(ret.body, paragraphProcessor);
+ } else {
+ $body = paragraphProcessor(ret.body);
+ }
+ ret.body = `${$body}
`;
return ret;
}
diff --git a/src/locales/zh_CN.js b/src/locales/zh_CN.js
index e8bd016f3..f2a88afff 100644
--- a/src/locales/zh_CN.js
+++ b/src/locales/zh_CN.js
@@ -79,6 +79,7 @@ export default {
hide: '隐藏(ctrl+0)', // 隐藏(ctrl+0)
exportToPdf: '导出PDF', // 导出PDF
exportScreenshot: '导出长图', // 导出长图
- theme: '主题', // 导出长图
- panel: '面板', // 导出长图
+ theme: '主题', // 主题
+ panel: '面板', // 面板
+ detail: '手风琴', // 手风琴
};
diff --git a/src/sass/markdown.scss b/src/sass/markdown.scss
index ab870711e..4f0711733 100644
--- a/src/sass/markdown.scss
+++ b/src/sass/markdown.scss
@@ -471,6 +471,38 @@ div[data-type='codeBlock'] {
}
}
+.cherry-detail {
+ details {
+ background: #f8f9faaa;
+ border-radius: 8px;
+ overflow: hidden;
+ margin-bottom: 10px;
+ summary {
+ user-select: none;
+ padding: 5px 10px;
+ background-color: #6c757d;
+ color: #FFF;
+ border-radius: 8px;
+ }
+ .cherry-detail-body {
+ padding: 15px 25px 0 25px;
+ }
+ }
+}
+
+.cherry-detail__multiple {
+ border-radius: 8px;
+ overflow: hidden;
+ details {
+ margin-bottom: 1px;
+ border-radius: 0;
+ border: none;
+ summary {
+ border-radius: 0;
+ }
+ }
+}
+
.cherry-panel {
margin: 10px 0;
overflow: hidden;
diff --git a/src/toolbars/HookCenter.js b/src/toolbars/HookCenter.js
index cfc8ee8c3..507849605 100644
--- a/src/toolbars/HookCenter.js
+++ b/src/toolbars/HookCenter.js
@@ -66,6 +66,7 @@ import Theme from './hooks/Theme';
import MobilePreview from './hooks/MobilePreview';
import Copy from './hooks/Copy';
import Panel from './hooks/Panel';
+import Detail from './hooks/Detail';
// 定义默认支持的工具栏
// 目前不支持按需动态加载
@@ -120,6 +121,7 @@ const HookList = {
theme: Theme,
file: File,
panel: Panel,
+ detail: Detail,
};
export default class HookCenter {
diff --git a/src/toolbars/hooks/Detail.js b/src/toolbars/hooks/Detail.js
new file mode 100644
index 000000000..447fa03b4
--- /dev/null
+++ b/src/toolbars/hooks/Detail.js
@@ -0,0 +1,64 @@
+/**
+ * Copyright (C) 2021 THL A29 Limited, a Tencent company.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import MenuBase from '@/toolbars/MenuBase';
+import { getDetailRule } from '@/utils/regexp';
+import { getSelection } from '@/utils/selection';
+/**
+ * 插入手风琴
+ */
+export default class Detail extends MenuBase {
+ constructor($cherry) {
+ super($cherry);
+ this.setName('detail', 'insertFlow');
+ this.detailRule = getDetailRule().reg;
+ }
+
+ /**
+ * 响应点击事件
+ * @param {string} selection 被用户选中的文本内容
+ * @returns {string} 回填到编辑器光标位置/选中文本区域的内容
+ */
+ onClick(selection) {
+ let $selection =
+ getSelection(this.editor.editor, selection, 'line', true) ||
+ '点击展开更多\n:\n内容\n+-\n默认展开\n:\n内容\n++\n默认收起\n:\n内容';
+ this.detailRule.lastIndex = 0;
+ if (!this.detailRule.test($selection)) {
+ // 如果没有命中手风琴语法,则尝试扩大选区
+ this.getMoreSelection('\n', '\n', () => {
+ const newSelection = this.editor.editor.getSelection();
+ this.detailRule.lastIndex = 0;
+ const isMatch = this.detailRule.test(newSelection);
+ if (isMatch !== false) {
+ $selection = newSelection;
+ }
+ return isMatch !== false;
+ });
+ }
+ this.detailRule.lastIndex = 0;
+ if (this.detailRule.test($selection)) {
+ // 如果命中了手风琴语法,则去掉手风琴语法
+ this.detailRule.lastIndex = 0;
+ return $selection.replace(this.detailRule, (match, preLines, isShow, content) => {
+ return content;
+ });
+ }
+ this.registerAfterClickCb(() => {
+ this.setLessSelection('\n', '\n');
+ });
+ return `++++\n${$selection}\n+++`.replace(/\n{2,}\+\+\+/g, '\n+++');
+ }
+}
diff --git a/src/toolbars/hooks/Panel.js b/src/toolbars/hooks/Panel.js
index fef86d2b1..33cac4c36 100644
--- a/src/toolbars/hooks/Panel.js
+++ b/src/toolbars/hooks/Panel.js
@@ -60,6 +60,7 @@ export default class Panel extends MenuBase {
*/
$getNameFromStr(str) {
let ret = false;
+ this.panelRule.lastIndex = 0;
str.replace(this.panelRule, (match, preLines, name, content) => {
ret = name ? name : '';
return match;
@@ -92,6 +93,7 @@ export default class Panel extends MenuBase {
// 如果命中了面板语法,则尝试去掉语法或者变更语法
if (currentName === shortKey) {
// 去掉面板语法
+ this.panelRule.lastIndex = 0;
return $selection.replace(this.panelRule, (match, preLines, name, content) => {
return content;
});
@@ -100,8 +102,9 @@ export default class Panel extends MenuBase {
this.registerAfterClickCb(() => {
this.setLessSelection('\n', '\n');
});
+ this.panelRule.lastIndex = 0;
return $selection.replace(this.panelRule, (match, preLines, name, content) => {
- return `:::${shortKey}\n${content}:::`;
+ return `:::${shortKey}\n${content.replace(/\n+$/, '')}\n:::`;
});
}
this.registerAfterClickCb(() => {
diff --git a/src/utils/regexp.js b/src/utils/regexp.js
index 837f0ddce..9de2a1df4 100644
--- a/src/utils/regexp.js
+++ b/src/utils/regexp.js
@@ -213,7 +213,36 @@ export function getPanelRule() {
const ret = {
begin: /(?:^|\n)(\n*(?:[^\S\n]*)):::([^:\n\s]+?)\s*\n/,
content: /([\w\W]*?)/,
- end: /[^\S\n]*:::[ \t]*(?=$|\n+)/,
+ end: /\n[ \t]*:::[ \t]*(?=$|\n+)/,
+ };
+ ret.reg = new RegExp(ret.begin.source + ret.content.source + ret.end.source, 'g');
+ return ret;
+}
+
+/**
+ * 手风琴/detail语法的识别正则
+ * 例:
+ * +++(+|-)
+ * 点击查看详情
+ * :
+ * body
+ * body
+ * +-
+ * 标题(默认收起内容)
+ * :
+ * 内容
+ * ++
+ * 标题(默认展开内容)
+ * :
+ * 内容2
+ * +++
+ * @returns {object}
+ */
+export function getDetailRule() {
+ const ret = {
+ begin: /(?:^|\n)(\n*(?:[^\S\n]*))\+\+\+(\+|-)\s*\n/,
+ content: /([\w\W]+?\n:\n[\w\W]+?)/,
+ end: /\n[ \t]*\+\+\+[ \t]*(?=$|\n+)/,
};
ret.reg = new RegExp(ret.begin.source + ret.content.source + ret.end.source, 'g');
return ret;
diff --git a/vscodePlugin/src/readme.md b/vscodePlugin/src/readme.md
new file mode 100644
index 000000000..772bc5006
--- /dev/null
+++ b/vscodePlugin/src/readme.md
@@ -0,0 +1,4 @@
+## 发布插件
+```
+vsce publish
+```