From b8e1e06133294e1e4c914a8548d833446956cd65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E9=91=AB=E6=B5=A9?= Date: Thu, 22 Jan 2026 12:09:03 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(code):=20=E5=A2=9E=E5=8A=A0=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=9D=97=E5=9B=9E=E8=BD=A6=E5=90=8E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=A1=A5=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/hooks/CodeBlock.js | 66 +++++-------------- .../src/core/hooks/Suggester.js | 52 +++++++++++---- 2 files changed, 54 insertions(+), 64 deletions(-) diff --git a/packages/cherry-markdown/src/core/hooks/CodeBlock.js b/packages/cherry-markdown/src/core/hooks/CodeBlock.js index ee6255577..17c59fc2b 100644 --- a/packages/cherry-markdown/src/core/hooks/CodeBlock.js +++ b/packages/cherry-markdown/src/core/hooks/CodeBlock.js @@ -18,7 +18,7 @@ import Prism from 'prismjs'; import { escapeHTMLSpecialChar } from '@/utils/sanitize'; import { getTableRule, getCodeBlockRule } from '@/utils/regexp'; import { prependLineFeedForParagraph } from '@/utils/lineFeed'; - +import Logger from '@/Logger'; Prism.manual = true; const CUSTOM_WRAPPER = { @@ -41,8 +41,8 @@ export default class CodeBlock extends ParagraphBase { this.expandCode = config.expandCode; // 是否显示“展开”按钮 this.editCode = config.editCode; // 是否显示“编辑”按钮 this.changeLang = config.changeLang; // 是否显示“切换语言”按钮 - this.selfClosing = config.selfClosing; // 自动闭合,为true时,当md中有奇数个```时,会自动在md末尾追加一个``` - this.mermaid = config.mermaid; // mermaid的配置,目前仅支持格式设置,svg2img=true 展示成图片,false 展示成svg + this.selfClosing = config.selfClosing; // 自动闭合,为 true 时,当 md 中有奇数个```时,会自动在 md 末尾追加一个``` + this.mermaid = config.mermaid; // mermaid 的配置,目前仅支持格式设置,svg2img=true 展示成图片,false 展示成 svg this.indentedCodeBlock = typeof config.indentedCodeBlock === 'undefined' ? true : config.indentedCodeBlock; // 是否支持缩进代码块 this.INLINE_CODE_REGEX = /(`+)(.+?(?:\n.+?)*?)\1/g; if (config && config.customRenderer) { @@ -83,7 +83,7 @@ export default class CodeBlock extends ParagraphBase { $resetCache() { if (this.codeCacheList.length > 100) { - // 如果缓存超过100条,则清空最早的缓存 + // 如果缓存超过 100 条,则清空最早的缓存 for (let i = 0; i < this.codeCacheList.length - 100; i++) { delete this.codeCache[this.codeCacheList[i]]; } @@ -218,7 +218,7 @@ export default class CodeBlock extends ParagraphBase { } /** - * 补齐用codeBlock承载的mermaid + * 补齐用 codeBlock 承载的 mermaid * @param {string} $code * @param {string} $lang */ @@ -235,7 +235,7 @@ export default class CodeBlock extends ParagraphBase { lang = 'mermaid'; } if (lang === 'mermaid') { - // 8.4.8版本兼容8.5.2版本的语法 + // 8.4.8 版本兼容 8.5.2 版本的语法 code = code.replace(/(^[\s]*)stateDiagram-v2\n/, '$1stateDiagram\n'); // code = code.replace(/(^[\s]*)sequenceDiagram[ \t]*\n[\s]*autonumber[ \t]*\n/, '$1sequenceDiagram\n'); } @@ -271,8 +271,8 @@ export default class CodeBlock extends ParagraphBase { // 平台自定义代码块样式 cacheCode = this.customHighlighter(cacheCode, lang); } else { - // 默认使用prism渲染代码块 - if (!lang || !Prism.languages[lang]) lang = 'javascript'; // 如果没有写语言,默认用js样式渲染 + // 默认使用 prism 渲染代码块 + if (!lang || !Prism.languages[lang]) lang = 'javascript'; // 如果没有写语言,默认用 js 样式渲染 cacheCode = Prism.highlight(cacheCode, Prism.languages[lang], lang); cacheCode = this.renderLineNumber(cacheCode); } @@ -366,43 +366,9 @@ export default class CodeBlock extends ParagraphBase { }); } - $dealUnclosingCode(str) { - const codes = str.match( - /(?:^|\n)(\n*((?:>[\t ]*)*)(?:[^\S\n]*))(`{3,})([^`]*?)(?=CHERRY_FLOW_SESSION_CURSOR|$|\n)/g, - ); - if (!codes || codes.length <= 0) { - return str; - } - // 剔除异常的数据 - let codeBegin = false; - const $codes = codes.filter((value) => { - if (codeBegin === false) { - codeBegin = true; - return true; - } - if (/```[^`\s]+/.test(value)) { - return false; - } - codeBegin = false; - return true; - }); - // 如果有奇数个代码块关键字,则进行自动闭合 - if ($codes.length % 2 === 1) { - const lastCode = $codes[$codes.length - 1].replace(/(`)[^`]+$/, '$1').replace(/\n+/, ''); - const $str = str.replace(/\n+$/, '').replace(/\n`{1,2}$/, ''); - return `${$str}\n${lastCode}\n`; - } - return str; - } - 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 +384,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 +414,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 +433,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 +451,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 +467,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 +487,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..22dc18302 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,30 @@ class SuggesterPanel { onCodeMirrorChange(codemirror, evt) { const { text, from, to, origin } = evt; const changeValue = text.length === 1 ? text[0] : ''; + // 【新增】处理 ``` 关键字的回车键特殊逻辑 + if (origin === '+input' ) { + const cursor = codemirror.getCursor(); + const lineContent = codemirror.getLine(cursor.line - 1); // 上一行内容 + // 检查是否以 ``` 开头(代码块开始标记) + if (/^[\s]*```/.test(lineContent)) { + + const language = lineContent.replace(/^[\s]*```\s*/, ''); // 提取语言标识符 + const cursorFrom = { line: 0, ch: 0 }; // 需要计算实际的起始位置 + + // 构建完整的代码块结构 + const codeBlock = `\`\`\`${language}\n\n\`\`\`\n`; + + // 替换原来的 ``` 语言为完整的代码块 + codemirror.replaceRange(codeBlock, { line: cursor.line - 1, ch: 0 }, { line: cursor.line, ch: 0 }); + + // 将光标移动到代码块内部(第二行开始位置) + codemirror.setCursor(cursor.line, language.length + 4); + + this.stopRelate(); // 停止联想 + return; // 不继续执行后续逻辑 + } + } // 首次输入命中关键词的时候开启联想 if (!this.enableRelate() && this.suggesterConfig[changeValue]) { this.startRelate(codemirror, changeValue, from); @@ -716,9 +740,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; @@ -805,9 +829,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 +853,7 @@ class SuggesterPanel { this.stopRelate(); }, 0); } else if (keyCode === 27 || keyCode === 0x25 || keyCode === 0x27) { - // 按下esc或者←、→键的时候退出联想 + // 按下 esc 或者←、→键的时候退出联想 evt.stopPropagation(); codemirror.focus(); setTimeout(() => { From 7589f31ae9bd336bb9faa39f8c37fd5e5c408dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E9=91=AB=E6=B5=A9?= Date: Thu, 22 Jan 2026 14:51:14 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(code):=20=E5=A4=84=E7=90=86=E4=BA=86?= =?UTF-8?q?=20mermaid=20=E7=B1=BB=E5=9E=8B=E7=9A=84=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=9D=97=E5=92=8C=E4=BF=AE=E4=BA=86=E4=B8=80=E4=BA=9B=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cherry-code-block-mermaid-plugin.js | 39 +++++++---- .../src/core/hooks/CodeBlock.js | 66 +++++++++++++------ .../src/core/hooks/Suggester.js | 49 +++++++------- 3 files changed, 95 insertions(+), 59 deletions(-) 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 = `${graphId}`; @@ -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 `
+
Mermaid 图表渲染中...
+
`; + } + + // 非流式模式下,先渲染源码 return props.fallback(); } - render(src, sign, $engine, props = {}) { let $sign = sign; if (!$sign) { $sign = Math.round(Math.random() * 100000000); } this.mountMermaidCanvas($engine); - // 多实例的情况下相同的内容ID相同会导致mermaid渲染异常 - // 需要通过添加时间戳使得多次渲染相同内容的图像ID唯一 - // 图像渲染节流在CodeBlock Hook内部控制 + // 多实例的情况下相同的内容 ID 相同会导致 mermaid 渲染异常 + // 需要通过添加时间戳使得多次渲染相同内容的图像 ID 唯一 + // 图像渲染节流在 CodeBlock Hook 内部控制 const graphId = `mermaid-${sign}-${new Date().getTime()}`; this.svg2img = props.mermaidConfig?.svg2img ?? false; return this.isAsyncRenderVersion() diff --git a/packages/cherry-markdown/src/core/hooks/CodeBlock.js b/packages/cherry-markdown/src/core/hooks/CodeBlock.js index 17c59fc2b..a48c51d6b 100644 --- a/packages/cherry-markdown/src/core/hooks/CodeBlock.js +++ b/packages/cherry-markdown/src/core/hooks/CodeBlock.js @@ -18,7 +18,6 @@ import Prism from 'prismjs'; import { escapeHTMLSpecialChar } from '@/utils/sanitize'; import { getTableRule, getCodeBlockRule } from '@/utils/regexp'; import { prependLineFeedForParagraph } from '@/utils/lineFeed'; -import Logger from '@/Logger'; Prism.manual = true; const CUSTOM_WRAPPER = { @@ -131,6 +130,23 @@ export default class CodeBlock extends ParagraphBase { }; let html = ''; const $codeSrc = this.needCleanFlowCursor ? codeSrc.replace(/CHERRYFLOWSESSIONCURSOR/, '') : codeSrc; + // 对于 mermaid 返回占位符 + if (lang === 'mermaid' && /^```mermaid(?:\n*```|\n*)$/.test(props.match)) { + // 不显示代码块 + const placeholder = `
Mermaid 图表渲染中...
`; + engine.render($codeSrc, props.sign, this.$engine, { + mermaidConfig: this.mermaid, + updateCache: (cacheCode) => { + this.$codeCache(props.sign, addContainer(cacheCode)); + this.pushCache(addContainer(cacheCode), props.sign, props.lines); + }, + fallback: () => { + const $code = this.$codeReplace($codeSrc, lang, props.sign, props.lines); + return $code; + }, + }); + return addContainer(placeholder); + } if (lang === 'all') { html = engine.render($codeSrc, props.sign, this.$engine, props.lang); } else { @@ -272,32 +288,40 @@ export default class CodeBlock extends ParagraphBase { cacheCode = this.customHighlighter(cacheCode, lang); } else { // 默认使用 prism 渲染代码块 - if (!lang || !Prism.languages[lang]) lang = 'javascript'; // 如果没有写语言,默认用 js 样式渲染 - cacheCode = Prism.highlight(cacheCode, Prism.languages[lang], lang); - cacheCode = this.renderLineNumber(cacheCode); + // 如果没有写语言,默认用 js 样式渲染;但如果写了 Prism 不支持的语言,保留原始语言 + if (!lang) { + lang = 'javascript'; + } else if (!Prism.languages[lang]) { + // 对于 Prism 不支持的语言,保留原始语言标识符,但不进行语法高亮 + lang = oldLang; // 使用原始语言标识符 + cacheCode = escapeHTMLSpecialChar($code); // 不进行语法高亮,只进行 HTML 转义 + } else { + cacheCode = Prism.highlight(cacheCode, Prism.languages[lang], lang); + cacheCode = this.renderLineNumber(cacheCode); + } } const needUnExpand = this.expandCode && $code.match(/\n/g)?.length > 10; // 是否需要收起代码块 const codeHtml = `
${this.wrapCode(cacheCode, lang)}
`; cacheCode = `
- ${this.customWrapperRender(oldLang, cacheCode, codeHtml)} - `; + data-sign="${sign}" + data-type="codeBlock" + data-lines="${lines}" + data-edit-code="${this.editCode}" + data-copy-code="${this.copyCode}" + data-expand-code="${this.expandCode}" + data-change-lang="${this.changeLang}" + data-lang="${lang}" + style="position:relative" + class="${needUnExpand ? 'cherry-code-unExpand' : 'cherry-code-expand'}" + > + ${this.customWrapperRender(oldLang, cacheCode, codeHtml)} + `; if (needUnExpand) { cacheCode += `
-
- -
-
`; +
+ +
+
`; } cacheCode += ''; return cacheCode; diff --git a/packages/cherry-markdown/src/core/hooks/Suggester.js b/packages/cherry-markdown/src/core/hooks/Suggester.js index 22dc18302..d3931d78c 100644 --- a/packages/cherry-markdown/src/core/hooks/Suggester.js +++ b/packages/cherry-markdown/src/core/hooks/Suggester.js @@ -699,30 +699,6 @@ class SuggesterPanel { onCodeMirrorChange(codemirror, evt) { const { text, from, to, origin } = evt; const changeValue = text.length === 1 ? text[0] : ''; - // 【新增】处理 ``` 关键字的回车键特殊逻辑 - if (origin === '+input' ) { - const cursor = codemirror.getCursor(); - const lineContent = codemirror.getLine(cursor.line - 1); // 上一行内容 - - // 检查是否以 ``` 开头(代码块开始标记) - if (/^[\s]*```/.test(lineContent)) { - - const language = lineContent.replace(/^[\s]*```\s*/, ''); // 提取语言标识符 - const cursorFrom = { line: 0, ch: 0 }; // 需要计算实际的起始位置 - - // 构建完整的代码块结构 - const codeBlock = `\`\`\`${language}\n\n\`\`\`\n`; - - // 替换原来的 ``` 语言为完整的代码块 - codemirror.replaceRange(codeBlock, { line: cursor.line - 1, ch: 0 }, { line: cursor.line, ch: 0 }); - - // 将光标移动到代码块内部(第二行开始位置) - codemirror.setCursor(cursor.line, language.length + 4); - - this.stopRelate(); // 停止联想 - return; // 不继续执行后续逻辑 - } - } // 首次输入命中关键词的时候开启联想 if (!this.enableRelate() && this.suggesterConfig[changeValue]) { this.startRelate(codemirror, changeValue, from); @@ -753,6 +729,31 @@ class SuggesterPanel { }); } } + // 处理 ``` 关键字的回车键特殊逻辑 + if (origin === '+input' && changeValue === '' ) { + const cursor = codemirror.getCursor(); + const lineContent = codemirror.getLine(cursor.line - 1); // 上一行内容 + const nextLineContent = codemirror.getLine(cursor.line + 1); // 当前行(也就是按回车后新行的内容) + + // Logger.debug('[CodeBlock]', '[onCodeMirrorChange]', 'lineContent', lineContent) + // Logger.debug('[CodeBlock]', '[onCodeMirrorChange]', 'nextLineContent', nextLineContent) + // 这样可以避免在已展开的代码块内部重复触发 + //如果相隔超过一行就不要管了,继续重新生成 + const backtickMatch = lineContent.match(/^[\s]*(```+)/); + if (/^[\s]*```[^\s]/.test(lineContent) && !/^[\s]*```\s*$/.test(nextLineContent)) { + const language = lineContent.replace(/^[\s]*```\s*/, ''); + const backticks = backtickMatch[1]; // 获取实际输入的 ``` 数量,如 ``` 或 ```` + + const codeBlock = `\`\`\`${language}\n\n${backticks}`; + codemirror.replaceRange(codeBlock, { line: cursor.line - 1, ch: 0 }, { line: cursor.line, ch: 0 }); + + // 将光标移动到代码块内部 + codemirror.setCursor(cursor.line, language.length + 4); + + this.stopRelate(); + return; + } + } } /** From cc3ccf16d5741111f048ff25e99b7f1bcac0b37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E9=91=AB=E6=B5=A9?= Date: Thu, 22 Jan 2026 14:51:44 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(code):=20=E8=81=94=E6=83=B3=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E4=B8=8B=E4=B9=9F=E8=83=BD=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cherry-markdown/src/core/hooks/CodeBlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cherry-markdown/src/core/hooks/CodeBlock.js b/packages/cherry-markdown/src/core/hooks/CodeBlock.js index a48c51d6b..2a10c3edc 100644 --- a/packages/cherry-markdown/src/core/hooks/CodeBlock.js +++ b/packages/cherry-markdown/src/core/hooks/CodeBlock.js @@ -288,7 +288,7 @@ export default class CodeBlock extends ParagraphBase { cacheCode = this.customHighlighter(cacheCode, lang); } else { // 默认使用 prism 渲染代码块 - // 如果没有写语言,默认用 js 样式渲染;但如果写了 Prism 不支持的语言,保留原始语言 + // 如果没有写语言,默认用 js 样式渲染;但如果写了 Prism 不支持的语言,保留原始语言 if (!lang) { lang = 'javascript'; } else if (!Prism.languages[lang]) { From 41368713304ca57a6fc34fa5b4b0190a8fcb2926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E9=91=AB=E6=B5=A9?= Date: Thu, 22 Jan 2026 23:23:02 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(code):=20=E4=BF=AE=E5=A4=8D=E4=BA=86=20?= =?UTF-8?q?Suggester=20=E7=9A=84=E9=80=BB=E8=BE=91=EF=BC=8C=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=BA=86=E5=85=B3=E9=94=AE=E5=AD=97=E7=9A=84=E5=9B=9E?= =?UTF-8?q?=E8=BD=A6=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/hooks/CodeBlock.js | 78 +++++++++++++------ .../src/core/hooks/Suggester.js | 47 +++++++---- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/packages/cherry-markdown/src/core/hooks/CodeBlock.js b/packages/cherry-markdown/src/core/hooks/CodeBlock.js index 2a10c3edc..4c8b6cdb4 100644 --- a/packages/cherry-markdown/src/core/hooks/CodeBlock.js +++ b/packages/cherry-markdown/src/core/hooks/CodeBlock.js @@ -18,6 +18,7 @@ import Prism from 'prismjs'; import { escapeHTMLSpecialChar } from '@/utils/sanitize'; import { getTableRule, getCodeBlockRule } from '@/utils/regexp'; import { prependLineFeedForParagraph } from '@/utils/lineFeed'; +import Logger from '@/Logger'; Prism.manual = true; const CUSTOM_WRAPPER = { @@ -130,23 +131,6 @@ export default class CodeBlock extends ParagraphBase { }; let html = ''; const $codeSrc = this.needCleanFlowCursor ? codeSrc.replace(/CHERRYFLOWSESSIONCURSOR/, '') : codeSrc; - // 对于 mermaid 返回占位符 - if (lang === 'mermaid' && /^```mermaid(?:\n*```|\n*)$/.test(props.match)) { - // 不显示代码块 - const placeholder = `
Mermaid 图表渲染中...
`; - engine.render($codeSrc, props.sign, this.$engine, { - mermaidConfig: this.mermaid, - updateCache: (cacheCode) => { - this.$codeCache(props.sign, addContainer(cacheCode)); - this.pushCache(addContainer(cacheCode), props.sign, props.lines); - }, - fallback: () => { - const $code = this.$codeReplace($codeSrc, lang, props.sign, props.lines); - return $code; - }, - }); - return addContainer(placeholder); - } if (lang === 'all') { html = engine.render($codeSrc, props.sign, this.$engine, props.lang); } else { @@ -288,15 +272,20 @@ export default class CodeBlock extends ParagraphBase { cacheCode = this.customHighlighter(cacheCode, lang); } else { // 默认使用 prism 渲染代码块 - // 如果没有写语言,默认用 js 样式渲染;但如果写了 Prism 不支持的语言,保留原始语言 if (!lang) { lang = 'javascript'; - } else if (!Prism.languages[lang]) { - // 对于 Prism 不支持的语言,保留原始语言标识符,但不进行语法高亮 - lang = oldLang; // 使用原始语言标识符 - cacheCode = escapeHTMLSpecialChar($code); // 不进行语法高亮,只进行 HTML 转义 - } else { + } + + // 如果 Prism 支持该语言,则进行高亮 + if (Prism.languages[lang]) { cacheCode = Prism.highlight(cacheCode, Prism.languages[lang], lang); + } else { + // Prism 不支持的语言,只进行 HTML 转义,保留原始语言标识符 + cacheCode = escapeHTMLSpecialChar(cacheCode); + } + + // 如果需要显示行号,则渲染行号 + if (this.lineNumber) { cacheCode = this.renderLineNumber(cacheCode); } } @@ -390,9 +379,50 @@ 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, + ); + if (!codes || codes.length <= 0) { + return str; + } + // 在流式输出模式下,如果有未闭合的代码块,不输出任何内容 + if (this.$cherry.options.engine.global.flowSessionContext) { + // 流式输出时,不自动闭合代码块,也不输出占位符 + // 只有当代码块完全确定时才输出 + return str; + } + // 剔除异常的数据 + let codeBegin = false; + const $codes = codes.filter((value) => { + if (codeBegin === false) { + codeBegin = true; + return true; + } + if (/```[^`\s]+/.test(value)) { + return false; + } + codeBegin = false; + return true; + }); + Logger.log($codes.length % 2 === 1) + // 如果有奇数个代码块关键字,则进行自动闭合 + if ($codes.length % 2 === 1) { + const lastCode = $codes[$codes.length - 1].replace(/(`)[^`]+$/, '$1').replace(/\n+/, ''); + const $str = str.replace(/\n+$/, '').replace(/\n`{1,2}$/, ''); + return `${$str}\n${lastCode}\n`; + } + return str; + } + beforeMakeHtml(str, sentenceMakeFunc, markdownParams) { let $str = str; - + // 处理段落代码块自动闭合 + if (this.selfClosing || this.$cherry.options.engine.global.flowSessionContext) { + $str = this.$dealUnclosingCode($str); + } // 预处理缩进代码块 $str = this.$replaceCodeInIndent($str); diff --git a/packages/cherry-markdown/src/core/hooks/Suggester.js b/packages/cherry-markdown/src/core/hooks/Suggester.js index d3931d78c..2e88dc560 100644 --- a/packages/cherry-markdown/src/core/hooks/Suggester.js +++ b/packages/cherry-markdown/src/core/hooks/Suggester.js @@ -728,28 +728,45 @@ class SuggesterPanel { this.updatePanel(this.optionList); }); } - } + } // 处理 ``` 关键字的回车键特殊逻辑 - if (origin === '+input' && changeValue === '' ) { + if (origin === '+input' && changeValue === '') { const cursor = codemirror.getCursor(); const lineContent = codemirror.getLine(cursor.line - 1); // 上一行内容 - const nextLineContent = codemirror.getLine(cursor.line + 1); // 当前行(也就是按回车后新行的内容) - // Logger.debug('[CodeBlock]', '[onCodeMirrorChange]', 'lineContent', lineContent) - // Logger.debug('[CodeBlock]', '[onCodeMirrorChange]', 'nextLineContent', nextLineContent) - // 这样可以避免在已展开的代码块内部重复触发 - //如果相隔超过一行就不要管了,继续重新生成 - const backtickMatch = lineContent.match(/^[\s]*(```+)/); - if (/^[\s]*```[^\s]/.test(lineContent) && !/^[\s]*```\s*$/.test(nextLineContent)) { - const language = lineContent.replace(/^[\s]*```\s*/, ''); - const backticks = backtickMatch[1]; // 获取实际输入的 ``` 数量,如 ``` 或 ```` + // 检查上一行是否是代码块开始标记(三个以上反引号 + 可选语言) + const backtickMatch = lineContent.match(/^[\s]*(`{3,})[^\s]*/); + if (backtickMatch) { + const backticks = backtickMatch[1]; // 获取实际输入的反引号数量 + const language = lineContent.replace(/^[\s]*`{3,}\s*/, ''); // 获取语言部分 - const codeBlock = `\`\`\`${language}\n\n${backticks}`; - codemirror.replaceRange(codeBlock, { line: cursor.line - 1, ch: 0 }, { line: cursor.line, ch: 0 }); + // 统计整个文档中代码块开始和结束标记的数量 + let codeBlockStarts = 0; + let codeBlockEnds = 0; + const totalLines = codemirror.lineCount(); - // 将光标移动到代码块内部 - codemirror.setCursor(cursor.line, language.length + 4); + 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; } From 4d6eb6bfb382dd53b56604c5f11dacd39c6a8173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E9=91=AB=E6=B5=A9?= Date: Fri, 23 Jan 2026 09:37:14 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(code):=20=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E4=B8=80=E4=BA=9B=E9=97=AE=E9=A2=98=EF=BC=8C=E4=B8=BB=E8=A6=81?= =?UTF-8?q?=E6=98=AF=E5=9B=9E=E9=80=80=E5=92=8C=E6=9B=B4=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/hooks/CodeBlock.js | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/packages/cherry-markdown/src/core/hooks/CodeBlock.js b/packages/cherry-markdown/src/core/hooks/CodeBlock.js index 4c8b6cdb4..f80b7a9c0 100644 --- a/packages/cherry-markdown/src/core/hooks/CodeBlock.js +++ b/packages/cherry-markdown/src/core/hooks/CodeBlock.js @@ -271,23 +271,9 @@ export default class CodeBlock extends ParagraphBase { // 平台自定义代码块样式 cacheCode = this.customHighlighter(cacheCode, lang); } else { - // 默认使用 prism 渲染代码块 - if (!lang) { - lang = 'javascript'; - } - - // 如果 Prism 支持该语言,则进行高亮 - if (Prism.languages[lang]) { - cacheCode = Prism.highlight(cacheCode, Prism.languages[lang], lang); - } else { - // Prism 不支持的语言,只进行 HTML 转义,保留原始语言标识符 - cacheCode = escapeHTMLSpecialChar(cacheCode); - } - - // 如果需要显示行号,则渲染行号 - if (this.lineNumber) { - cacheCode = this.renderLineNumber(cacheCode); - } + if (!lang || !Prism.languages[lang]) lang = 'javascript'; // 如果没有写语言,默认用 js 样式渲染 + cacheCode = Prism.highlight(cacheCode, Prism.languages[lang], lang); + cacheCode = this.renderLineNumber(cacheCode); } const needUnExpand = this.expandCode && $code.match(/\n/g)?.length > 10; // 是否需要收起代码块 const codeHtml = `
${this.wrapCode(cacheCode, lang)}
`; @@ -379,7 +365,6 @@ export default class CodeBlock extends ParagraphBase { }); } - //现在这里要做的只是把 mermaid 的排除逻辑放进来 ? r 然后其他的就不要管了,尽量不要影响其他功能 $dealUnclosingCode(str) { const codes = str.match( @@ -388,12 +373,6 @@ export default class CodeBlock extends ParagraphBase { if (!codes || codes.length <= 0) { return str; } - // 在流式输出模式下,如果有未闭合的代码块,不输出任何内容 - if (this.$cherry.options.engine.global.flowSessionContext) { - // 流式输出时,不自动闭合代码块,也不输出占位符 - // 只有当代码块完全确定时才输出 - return str; - } // 剔除异常的数据 let codeBegin = false; const $codes = codes.filter((value) => { @@ -407,9 +386,11 @@ export default class CodeBlock extends ParagraphBase { codeBegin = false; return true; }); - Logger.log($codes.length % 2 === 1) - // 如果有奇数个代码块关键字,则进行自动闭合 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`;