diff --git a/packages/cherry-markdown/src/addons/cherry-code-block-mermaid-plugin.js b/packages/cherry-markdown/src/addons/cherry-code-block-mermaid-plugin.js
index 16a692934..1773b4831 100644
--- a/packages/cherry-markdown/src/addons/cherry-code-block-mermaid-plugin.js
+++ b/packages/cherry-markdown/src/addons/cherry-code-block-mermaid-plugin.js
@@ -15,6 +15,7 @@
*/
import mergeWith from 'lodash/mergeWith';
import { isBrowser } from '@/utils/env';
+import Logger from '@/Logger';
const CHART_TYPES = [
'flowchart',
@@ -85,7 +86,7 @@ export default class MermaidCodeEngine {
* @param {Object} mermaidOptions - Mermaid 配置选项
* @param {Object} [mermaidOptions.mermaid] - mermaid 实例对象,如果未提供会尝试从 window.mermaid 获取
* @param {Object} [mermaidOptions.mermaidAPI] - mermaidAPI 实例对象,如果未提供会尝试从 window.mermaidAPI 获取
- * @param {string} [mermaidOptions.theme='default'] - 主题,可选值: 'default', 'dark', 'forest', 'neutral' 等
+ * @param {string} [mermaidOptions.theme='default'] - 主题,可选值:'default', 'dark', 'forest', 'neutral' 等
* @param {string} [mermaidOptions.altFontFamily='sans-serif'] - 备用字体
* @param {string} [mermaidOptions.fontFamily='sans-serif'] - 主字体
* @param {string} [mermaidOptions.themeCSS] - 自定义主题 CSS 样式
@@ -151,7 +152,7 @@ export default class MermaidCodeEngine {
}
/**
- * 转换svg为img,如果出错则直出svg
+ * 转换 svg 为 img,如果出错则直出 svg
* @param {string} svgCode
* @param {string} graphId
* @returns {string}
@@ -164,7 +165,7 @@ export default class MermaidCodeEngine {
try {
const svgDoc = /** @type {XMLDocument} */ (domParser.parseFromString(svgCode, 'image/svg+xml'));
const svgDom = /** @type {SVGSVGElement} */ (/** @type {any} */ (svgDoc.documentElement));
- // tagName不是svg时,说明存在parse error
+ // tagName 不是 svg 时,说明存在 parse error
if (svgDom.tagName.toLowerCase() === 'svg') {
svgDom.style.maxWidth = '100%';
svgDom.style.height = 'auto';
@@ -180,7 +181,7 @@ export default class MermaidCodeEngine {
svgDom.getAttribute('height') === '100%' && svgDom.setAttribute('height', `${svgBox.height}`);
// fix end
svgHtml = svgDoc.documentElement.outerHTML;
- // 屏蔽转img标签功能,如需要转换为img解除屏蔽即可
+ // 屏蔽转 img 标签功能,如需要转换为 img 解除屏蔽即可
if (this.svg2img) {
const dataUrl = `data:image/svg+xml,${encodeURIComponent(svgDoc.documentElement.outerHTML)}`;
svgHtml = ``;
@@ -219,8 +220,8 @@ export default class MermaidCodeEngine {
/**
* 如果开启了流式渲染,当前有上次渲染结果时,使用上次渲染结果
* 这里有赌的成分
- * 流式输出场景,只有最后一个mermaid代码块在流式输出,随着最后一个mermaid流式输出,mermaid的渲染有概率会失败
- * 这里赌的是只有一个mermaid代码块需要渲染
+ * 流式输出场景,只有最后一个 mermaid 代码块在流式输出,随着最后一个 mermaid 流式输出,mermaid 的渲染有概率会失败
+ * 这里赌的是只有一个 mermaid 代码块需要渲染
*/
if ($engine.$cherry.options.engine.global.flowSessionContext && this.lastRenderedCode) {
return this.lastRenderedCode;
@@ -249,6 +250,7 @@ export default class MermaidCodeEngine {
asyncRender(graphId, src, sign, $engine, props) {
$engine.asyncRenderHandler.add(graphId);
+
this.mermaidAPIRefs
.render(graphId, src, this.mermaidCanvas)
.then(({ svg: svgCode }) => {
@@ -260,10 +262,10 @@ export default class MermaidCodeEngine {
.catch(() => {
/**
* 如果开启了流式渲染,当前有上次渲染结果时,使用上次渲染结果
- * 这里有赌的成分,流式输出场景,只有最后一个mermaid代码块在流式输出,随着最后一个mermaid流式输出,mermaid的渲染有概率会失败
+ * 这里有赌的成分,流式输出场景,只有最后一个 mermaid 代码块在流式输出,随着最后一个 mermaid 流式输出,mermaid 的渲染有概率会失败
* 这里赌的是:
- * 1、只有一个mermaid代码块需要渲染
- * 2、纯预览模式,且流式输出场景,所有mermaid都正常输出
+ * 1、只有一个 mermaid 代码块需要渲染
+ * 2、纯预览模式,且流式输出场景,所有 mermaid 都正常输出
*/
if (
$engine.$cherry.options.engine.global.flowSessionContext &&
@@ -278,22 +280,31 @@ export default class MermaidCodeEngine {
this.handleAsyncRenderDone(graphId, sign, $engine, props, html);
}
});
+ Logger.log('Mermaid async render started:', { graphId, sign });
if (this.needReturnLastRenderedCode) {
return this.lastRenderedCode;
}
- // 先渲染源码
+ Logger.log('Mermaid async render done:', { graphId, sign });
+
+ // 【关键修改】在流式渲染模式下,不显示代码块,而是显示占位符
+ if ($engine.$cherry.options.engine.global.flowSessionContext) {
+ return `
${this.wrapCode(cacheCode, lang)}`;
cacheCode = ``;
}
cacheCode += '';
return cacheCode;
@@ -366,6 +365,7 @@ export default class CodeBlock extends ParagraphBase {
});
}
+ //现在这里要做的只是把 mermaid 的排除逻辑放进来 ? r 然后其他的就不要管了,尽量不要影响其他功能
$dealUnclosingCode(str) {
const codes = str.match(
/(?:^|\n)(\n*((?:>[\t ]*)*)(?:[^\S\n]*))(`{3,})([^`]*?)(?=CHERRY_FLOW_SESSION_CURSOR|$|\n)/g,
@@ -386,8 +386,11 @@ export default class CodeBlock extends ParagraphBase {
codeBegin = false;
return true;
});
- // 如果有奇数个代码块关键字,则进行自动闭合
if ($codes.length % 2 === 1) {
+ // 在流式输出模式下,如果有未闭合的代码块,不输出任何内容
+ if (this.$cherry.options.engine.global.flowSessionContext) {
+ return str;
+ }
const lastCode = $codes[$codes.length - 1].replace(/(`)[^`]+$/, '$1').replace(/\n+/, '');
const $str = str.replace(/\n+$/, '').replace(/\n`{1,2}$/, '');
return `${$str}\n${lastCode}\n`;
@@ -397,12 +400,10 @@ export default class CodeBlock extends ParagraphBase {
beforeMakeHtml(str, sentenceMakeFunc, markdownParams) {
let $str = str;
-
// 处理段落代码块自动闭合
if (this.selfClosing || this.$cherry.options.engine.global.flowSessionContext) {
$str = this.$dealUnclosingCode($str);
}
-
// 预处理缩进代码块
$str = this.$replaceCodeInIndent($str);
@@ -418,7 +419,7 @@ export default class CodeBlock extends ParagraphBase {
}
let $code = code;
const { sign, lines } = this.computeLines(match, leadingContent, code);
- // 从缓存中获取html
+ // 从缓存中获取 html
let cacheCode = this.$codeCache(sign);
if (cacheCode && cacheCode !== '') {
// 别忘了把 ">"(引用块)加回来
@@ -448,7 +449,7 @@ export default class CodeBlock extends ParagraphBase {
// 如果是公式关键字,则直接返回
if (/^(math|katex|latex)$/i.test($lang) && !this.isInternalCustomLangCovered($lang)) {
const prefix = match.match(/^\s*/g);
- // ~D为经编辑器中间转义后的$,code结尾包含结束```前的所有换行符,所以不需要补换行
+ // ~D 为经编辑器中间转义后的$,code 结尾包含结束```前的所有换行符,所以不需要补换行
return `${prefix}~D~D\n${$code}~D~D`; // 提供公式语法供公式钩子解析
}
[$code, $lang] = this.appendMermaid($code, $lang);
@@ -467,14 +468,14 @@ export default class CodeBlock extends ParagraphBase {
this.$codeCache(sign, cacheCode);
return this.getCacheWithSpace(this.pushCache(cacheCode, sign, lines), match);
}
- // 渲染出错则按正常code进行渲染
+ // 渲染出错则按正常 code 进行渲染
}
// $code = this.$replaceSpecialChar($code);
cacheCode = this.$codeReplace($code, $lang, sign, lines);
const result = this.getCacheWithSpace(this.pushCache(cacheCode, sign, lines), match);
return addBlockQuoteSignToResult(result);
});
- // 表格里处理行内代码,让一个td里的行内代码语法生效,让跨td的行内代码语法失效
+ // 表格里处理行内代码,让一个 td 里的行内代码语法生效,让跨 td 的行内代码语法失效
$str = $str.replace(getTableRule(true), (whole, ...args) => {
return whole
.replace(/\\\|/g, '~CHERRYNormalLine')
@@ -485,8 +486,8 @@ export default class CodeBlock extends ParagraphBase {
.join('|')
.replace(/`/g, '\\`');
});
- // 为了避免InlineCode被HtmlBlock转义,需要在这里提前缓存
- // InlineBlock只需要在afterMakeHtml还原即可
+ // 为了避免 InlineCode 被 HtmlBlock 转义,需要在这里提前缓存
+ // InlineBlock 只需要在 afterMakeHtml 还原即可
$str = this.makeInlineCode($str, true);
// 处理缩进代码块
@@ -501,7 +502,7 @@ export default class CodeBlock extends ParagraphBase {
* @returns {string} 格式化后的语言
*/
formatLang(lang) {
- // 增加一个潜规则,即便配置了all,也不处理mermaid
+ // 增加一个潜规则,即便配置了 all,也不处理 mermaid
if (this.customLang.indexOf('all') !== -1 && lang !== 'mermaid') {
return 'all';
}
@@ -521,7 +522,7 @@ export default class CodeBlock extends ParagraphBase {
$code = $code.replace(/~CHERRYNormalLine/g, '|');
$code = $code.replace(/\\/g, '\\\\');
- // 如果行内代码只有一个颜色值,则在code末尾追加一个颜色圆点
+ // 如果行内代码只有一个颜色值,则在 code 末尾追加一个颜色圆点
const trimmed = $code.trim();
const isHex = /^#([0-9a-fA-F]{6})$/i.test(trimmed);
const isRgb = /^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/i.test(trimmed);
diff --git a/packages/cherry-markdown/src/core/hooks/Suggester.js b/packages/cherry-markdown/src/core/hooks/Suggester.js
index 88ae09566..2e88dc560 100644
--- a/packages/cherry-markdown/src/core/hooks/Suggester.js
+++ b/packages/cherry-markdown/src/core/hooks/Suggester.js
@@ -26,6 +26,7 @@ import { Pass } from 'codemirror/src/util/misc';
import { isLookbehindSupported } from '@/utils/regexp';
import { replaceLookbehind } from '@/utils/lookbehind-replace';
import { isBrowser } from '@/utils/env';
+import Logger from '@/Logger';
/**
* @typedef {import('codemirror')} CodeMirror
@@ -58,7 +59,7 @@ export default class Suggester extends SyntaxBase {
/**
* config.suggester 内容
* [{
- * 请求url
+ * 请求 url
suggestList: '',
唤醒关键字
keyword: '@',
@@ -87,7 +88,7 @@ export default class Suggester extends SyntaxBase {
}
afterInit(callback) {
- // node环境下直接跳过输入联想
+ // node 环境下直接跳过输入联想
if (!isBrowser()) {
return;
}
@@ -111,7 +112,7 @@ export default class Suggester extends SyntaxBase {
defaultSuggest.push({
keyword: suggesterKeyword,
suggestList(_word, callback) {
- // 将word全转成小写
+ // 将 word 全转成小写
const word = _word.toLowerCase();
const systemSuggestList = allSuggestList(
suggesterKeyword,
@@ -125,7 +126,7 @@ export default class Suggester extends SyntaxBase {
}
const keyword = word
.replace(/\s+/g, '') // 删掉空格,避免产生不必要的空数组元素
- .replace(new RegExp(`^${suggesterKeyword}`, 'g'), '') // 删掉word当中suggesterKeywords出现的字符
+ .replace(new RegExp(`^${suggesterKeyword}`, 'g'), '') // 删掉 word 当中 suggesterKeywords 出现的字符
.replace(/^[#]+/, '#')
.replace(/^[/]+/, '/')
.split('')
@@ -166,7 +167,7 @@ export default class Suggester extends SyntaxBase {
this.suggester[configItem.keyword] = configItem;
});
- // 反复初始化时, 缓存还在, dom 已更新情况
+ // 反复初始化时,缓存还在,dom 已更新情况
if (this.suggesterPanel.hasEditor()) {
this.suggesterPanel.editor = null;
}
@@ -395,7 +396,7 @@ class SuggesterPanel {
}
/**
- * 更新suggesterPanel
+ * 更新 suggesterPanel
* @param {SuggestList} suggestList
*/
updatePanel(suggestList) {
@@ -434,7 +435,7 @@ class SuggesterPanel {
}
/**
- * 渲染suggesterPanel item
+ * 渲染 suggesterPanel item
* @param {string} item 渲染内容
* @param {boolean} selected 是否选中
* @returns {string} html
@@ -468,7 +469,7 @@ class SuggesterPanel {
relocatePanel(codemirror) {
// 找到光标位置来确定候选框位置
let $cursor = this.$cherry.wrapperDom.querySelector('.CodeMirror-cursors .CodeMirror-cursor');
- // 当editor选中某一内容时,".CodeMirror-cursor"会消失,此时通过定位".selected"来确定候选框位置
+ // 当 editor 选中某一内容时,".CodeMirror-cursor"会消失,此时通过定位".selected"来确定候选框位置
if (!$cursor) {
$cursor = this.$cherry.wrapperDom.querySelector('.CodeMirror-selected');
}
@@ -690,7 +691,7 @@ class SuggesterPanel {
}
/**
- * codeMirror change事件
+ * codeMirror change 事件
* @param {CodeMirror.Editor} codemirror
* @param {CodeMirror.EditorChange} evt
* @returns
@@ -698,7 +699,6 @@ class SuggesterPanel {
onCodeMirrorChange(codemirror, evt) {
const { text, from, to, origin } = evt;
const changeValue = text.length === 1 ? text[0] : '';
-
// 首次输入命中关键词的时候开启联想
if (!this.enableRelate() && this.suggesterConfig[changeValue]) {
this.startRelate(codemirror, changeValue, from);
@@ -716,9 +716,9 @@ class SuggesterPanel {
}
// 展示推荐列表
if (typeof this.suggesterConfig[this.keyword]?.suggestList === 'function') {
- // 请求api 返回结果拼凑
+ // 请求 api 返回结果拼凑
this.suggesterConfig[this.keyword].suggestList(this.searchKeyCache.join(''), (res) => {
- // 如果返回了false,则强制退出联想
+ // 如果返回了 false,则强制退出联想
if (res === false) {
this.stopRelate();
return;
@@ -728,6 +728,48 @@ class SuggesterPanel {
this.updatePanel(this.optionList);
});
}
+ }
+ // 处理 ``` 关键字的回车键特殊逻辑
+ if (origin === '+input' && changeValue === '') {
+ const cursor = codemirror.getCursor();
+ const lineContent = codemirror.getLine(cursor.line - 1); // 上一行内容
+
+ // 检查上一行是否是代码块开始标记(三个以上反引号 + 可选语言)
+ const backtickMatch = lineContent.match(/^[\s]*(`{3,})[^\s]*/);
+ if (backtickMatch) {
+ const backticks = backtickMatch[1]; // 获取实际输入的反引号数量
+ const language = lineContent.replace(/^[\s]*`{3,}\s*/, ''); // 获取语言部分
+
+ // 统计整个文档中代码块开始和结束标记的数量
+ let codeBlockStarts = 0;
+ let codeBlockEnds = 0;
+ const totalLines = codemirror.lineCount();
+
+ for (let i = 0; i < totalLines; i++) {
+ const lineText = codemirror.getLine(i);
+
+ // 检查是否为代码块开始标记(三个以上反引号开头,且后面有非空白字符)
+ if (/^\s*`{3,}[^\s]/.test(lineText)) {
+ codeBlockStarts++;
+ }
+ // 检查是否为代码块结束标记(三个以上反引号开头,后面只有空白)
+ else if (/^\s*`{3,}\s*$/.test(lineText)) {
+ codeBlockEnds++;
+ }
+ }
+ // 是否存在未闭合的代码块
+ const hasUnclosedCodeBlockBefore = (codeBlockStarts + codeBlockEnds) % 2 === 1;
+ // Logger.log('hasUnclosedCodeBlockBefore', hasUnclosedCodeBlockBefore);
+
+ if (hasUnclosedCodeBlockBefore) {
+ // 存在未闭合,应该补全整个代码块结构
+ const codeBlock = `${backticks}${language}\n\n${backticks}`;
+ codemirror.replaceRange(codeBlock, { line: cursor.line - 1, ch: 0 }, { line: cursor.line, ch: 0 });
+ codemirror.setCursor(cursor.line, language.length + backticks.length + 1);
+ }
+ this.stopRelate();
+ return;
+ }
}
}
@@ -805,9 +847,9 @@ class SuggesterPanel {
const viewTop = this.$suggesterPanel.scrollTop;
// 可视区域范围下端
const viewBottom = viewTop + suggestPanelHeight;
- // item的上端
+ // item 的上端
const nextEleTop = nextElement.offsetTop;
- // item高度
+ // item 高度
const nextEleHeight = nextElement.offsetHeight;
// 当前元素全部或部分在可视区域之外,就滚动
if (nextEleTop < viewTop || nextEleTop + nextEleHeight > viewBottom) {
@@ -829,7 +871,7 @@ class SuggesterPanel {
this.stopRelate();
}, 0);
} else if (keyCode === 27 || keyCode === 0x25 || keyCode === 0x27) {
- // 按下esc或者←、→键的时候退出联想
+ // 按下 esc 或者←、→键的时候退出联想
evt.stopPropagation();
codemirror.focus();
setTimeout(() => {