Skip to content

Commit 7589f31

Browse files
committed
feat(code): 处理了 mermaid 类型的代码块和修了一些问题
1 parent b8e1e06 commit 7589f31

3 files changed

Lines changed: 95 additions & 59 deletions

File tree

packages/cherry-markdown/src/addons/cherry-code-block-mermaid-plugin.js

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
import mergeWith from 'lodash/mergeWith';
1717
import { isBrowser } from '@/utils/env';
18+
import Logger from '@/Logger';
1819

1920
const CHART_TYPES = [
2021
'flowchart',
@@ -85,7 +86,7 @@ export default class MermaidCodeEngine {
8586
* @param {Object} mermaidOptions - Mermaid 配置选项
8687
* @param {Object} [mermaidOptions.mermaid] - mermaid 实例对象,如果未提供会尝试从 window.mermaid 获取
8788
* @param {Object} [mermaidOptions.mermaidAPI] - mermaidAPI 实例对象,如果未提供会尝试从 window.mermaidAPI 获取
88-
* @param {string} [mermaidOptions.theme='default'] - 主题,可选值: 'default', 'dark', 'forest', 'neutral' 等
89+
* @param {string} [mermaidOptions.theme='default'] - 主题,可选值'default', 'dark', 'forest', 'neutral' 等
8990
* @param {string} [mermaidOptions.altFontFamily='sans-serif'] - 备用字体
9091
* @param {string} [mermaidOptions.fontFamily='sans-serif'] - 主字体
9192
* @param {string} [mermaidOptions.themeCSS] - 自定义主题 CSS 样式
@@ -151,7 +152,7 @@ export default class MermaidCodeEngine {
151152
}
152153

153154
/**
154-
* 转换svg为img,如果出错则直出svg
155+
* 转换 svg 为 img,如果出错则直出 svg
155156
* @param {string} svgCode
156157
* @param {string} graphId
157158
* @returns {string}
@@ -164,7 +165,7 @@ export default class MermaidCodeEngine {
164165
try {
165166
const svgDoc = /** @type {XMLDocument} */ (domParser.parseFromString(svgCode, 'image/svg+xml'));
166167
const svgDom = /** @type {SVGSVGElement} */ (/** @type {any} */ (svgDoc.documentElement));
167-
// tagName不是svg时,说明存在parse error
168+
// tagName 不是 svg 时,说明存在 parse error
168169
if (svgDom.tagName.toLowerCase() === 'svg') {
169170
svgDom.style.maxWidth = '100%';
170171
svgDom.style.height = 'auto';
@@ -180,7 +181,7 @@ export default class MermaidCodeEngine {
180181
svgDom.getAttribute('height') === '100%' && svgDom.setAttribute('height', `${svgBox.height}`);
181182
// fix end
182183
svgHtml = svgDoc.documentElement.outerHTML;
183-
// 屏蔽转img标签功能,如需要转换为img解除屏蔽即可
184+
// 屏蔽转 img 标签功能,如需要转换为 img 解除屏蔽即可
184185
if (this.svg2img) {
185186
const dataUrl = `data:image/svg+xml,${encodeURIComponent(svgDoc.documentElement.outerHTML)}`;
186187
svgHtml = `<img class="svg-img" src="${dataUrl}" alt="${graphId}" />`;
@@ -219,8 +220,8 @@ export default class MermaidCodeEngine {
219220
/**
220221
* 如果开启了流式渲染,当前有上次渲染结果时,使用上次渲染结果
221222
* 这里有赌的成分
222-
* 流式输出场景,只有最后一个mermaid代码块在流式输出,随着最后一个mermaid流式输出,mermaid的渲染有概率会失败
223-
* 这里赌的是只有一个mermaid代码块需要渲染
223+
* 流式输出场景,只有最后一个 mermaid 代码块在流式输出,随着最后一个 mermaid 流式输出,mermaid 的渲染有概率会失败
224+
* 这里赌的是只有一个 mermaid 代码块需要渲染
224225
*/
225226
if ($engine.$cherry.options.engine.global.flowSessionContext && this.lastRenderedCode) {
226227
return this.lastRenderedCode;
@@ -249,6 +250,7 @@ export default class MermaidCodeEngine {
249250

250251
asyncRender(graphId, src, sign, $engine, props) {
251252
$engine.asyncRenderHandler.add(graphId);
253+
252254
this.mermaidAPIRefs
253255
.render(graphId, src, this.mermaidCanvas)
254256
.then(({ svg: svgCode }) => {
@@ -260,10 +262,10 @@ export default class MermaidCodeEngine {
260262
.catch(() => {
261263
/**
262264
* 如果开启了流式渲染,当前有上次渲染结果时,使用上次渲染结果
263-
* 这里有赌的成分,流式输出场景,只有最后一个mermaid代码块在流式输出,随着最后一个mermaid流式输出,mermaid的渲染有概率会失败
265+
* 这里有赌的成分流式输出场景,只有最后一个 mermaid 代码块在流式输出,随着最后一个 mermaid 流式输出,mermaid 的渲染有概率会失败
264266
* 这里赌的是:
265-
* 1、只有一个mermaid代码块需要渲染
266-
* 2、纯预览模式,且流式输出场景,所有mermaid都正常输出
267+
* 1、只有一个 mermaid 代码块需要渲染
268+
* 2、纯预览模式,且流式输出场景,所有 mermaid 都正常输出
267269
*/
268270
if (
269271
$engine.$cherry.options.engine.global.flowSessionContext &&
@@ -278,22 +280,31 @@ export default class MermaidCodeEngine {
278280
this.handleAsyncRenderDone(graphId, sign, $engine, props, html);
279281
}
280282
});
283+
Logger.log('Mermaid async render started:', { graphId, sign });
281284
if (this.needReturnLastRenderedCode) {
282285
return this.lastRenderedCode;
283286
}
284-
// 先渲染源码
287+
Logger.log('Mermaid async render done:', { graphId, sign });
288+
289+
// 【关键修改】在流式渲染模式下,不显示代码块,而是显示占位符
290+
if ($engine.$cherry.options.engine.global.flowSessionContext) {
291+
return `<div data-sign="${sign}" data-type="codeBlock" class="mermaid-loading">
292+
<div class="mermaid-placeholder">Mermaid 图表渲染中...</div>
293+
</div>`;
294+
}
295+
296+
// 非流式模式下,先渲染源码
285297
return props.fallback();
286298
}
287-
288299
render(src, sign, $engine, props = {}) {
289300
let $sign = sign;
290301
if (!$sign) {
291302
$sign = Math.round(Math.random() * 100000000);
292303
}
293304
this.mountMermaidCanvas($engine);
294-
// 多实例的情况下相同的内容ID相同会导致mermaid渲染异常
295-
// 需要通过添加时间戳使得多次渲染相同内容的图像ID唯一
296-
// 图像渲染节流在CodeBlock Hook内部控制
305+
// 多实例的情况下相同的内容 ID 相同会导致 mermaid 渲染异常
306+
// 需要通过添加时间戳使得多次渲染相同内容的图像 ID 唯一
307+
// 图像渲染节流在 CodeBlock Hook 内部控制
297308
const graphId = `mermaid-${sign}-${new Date().getTime()}`;
298309
this.svg2img = props.mermaidConfig?.svg2img ?? false;
299310
return this.isAsyncRenderVersion()

packages/cherry-markdown/src/core/hooks/CodeBlock.js

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import Prism from 'prismjs';
1818
import { escapeHTMLSpecialChar } from '@/utils/sanitize';
1919
import { getTableRule, getCodeBlockRule } from '@/utils/regexp';
2020
import { prependLineFeedForParagraph } from '@/utils/lineFeed';
21-
import Logger from '@/Logger';
2221
Prism.manual = true;
2322

2423
const CUSTOM_WRAPPER = {
@@ -131,6 +130,23 @@ export default class CodeBlock extends ParagraphBase {
131130
};
132131
let html = '';
133132
const $codeSrc = this.needCleanFlowCursor ? codeSrc.replace(/CHERRYFLOWSESSIONCURSOR/, '') : codeSrc;
133+
// 对于 mermaid 返回占位符
134+
if (lang === 'mermaid' && /^```mermaid(?:\n*```|\n*)$/.test(props.match)) {
135+
// 不显示代码块
136+
const placeholder = `<div class="mermaid-loading">Mermaid 图表渲染中...</div>`;
137+
engine.render($codeSrc, props.sign, this.$engine, {
138+
mermaidConfig: this.mermaid,
139+
updateCache: (cacheCode) => {
140+
this.$codeCache(props.sign, addContainer(cacheCode));
141+
this.pushCache(addContainer(cacheCode), props.sign, props.lines);
142+
},
143+
fallback: () => {
144+
const $code = this.$codeReplace($codeSrc, lang, props.sign, props.lines);
145+
return $code;
146+
},
147+
});
148+
return addContainer(placeholder);
149+
}
134150
if (lang === 'all') {
135151
html = engine.render($codeSrc, props.sign, this.$engine, props.lang);
136152
} else {
@@ -272,32 +288,40 @@ export default class CodeBlock extends ParagraphBase {
272288
cacheCode = this.customHighlighter(cacheCode, lang);
273289
} else {
274290
// 默认使用 prism 渲染代码块
275-
if (!lang || !Prism.languages[lang]) lang = 'javascript'; // 如果没有写语言,默认用 js 样式渲染
276-
cacheCode = Prism.highlight(cacheCode, Prism.languages[lang], lang);
277-
cacheCode = this.renderLineNumber(cacheCode);
291+
// 如果没有写语言,默认用 js 样式渲染;但如果写了 Prism 不支持的语言,保留原始语言
292+
if (!lang) {
293+
lang = 'javascript';
294+
} else if (!Prism.languages[lang]) {
295+
// 对于 Prism 不支持的语言,保留原始语言标识符,但不进行语法高亮
296+
lang = oldLang; // 使用原始语言标识符
297+
cacheCode = escapeHTMLSpecialChar($code); // 不进行语法高亮,只进行 HTML 转义
298+
} else {
299+
cacheCode = Prism.highlight(cacheCode, Prism.languages[lang], lang);
300+
cacheCode = this.renderLineNumber(cacheCode);
301+
}
278302
}
279303
const needUnExpand = this.expandCode && $code.match(/\n/g)?.length > 10; // 是否需要收起代码块
280304
const codeHtml = `<pre class="language-${lang}">${this.wrapCode(cacheCode, lang)}</pre>`;
281305
cacheCode = `<div
282-
data-sign="${sign}"
283-
data-type="codeBlock"
284-
data-lines="${lines}"
285-
data-edit-code="${this.editCode}"
286-
data-copy-code="${this.copyCode}"
287-
data-expand-code="${this.expandCode}"
288-
data-change-lang="${this.changeLang}"
289-
data-lang="${lang}"
290-
style="position:relative"
291-
class="${needUnExpand ? 'cherry-code-unExpand' : 'cherry-code-expand'}"
292-
>
293-
${this.customWrapperRender(oldLang, cacheCode, codeHtml)}
294-
`;
306+
data-sign="${sign}"
307+
data-type="codeBlock"
308+
data-lines="${lines}"
309+
data-edit-code="${this.editCode}"
310+
data-copy-code="${this.copyCode}"
311+
data-expand-code="${this.expandCode}"
312+
data-change-lang="${this.changeLang}"
313+
data-lang="${lang}"
314+
style="position:relative"
315+
class="${needUnExpand ? 'cherry-code-unExpand' : 'cherry-code-expand'}"
316+
>
317+
${this.customWrapperRender(oldLang, cacheCode, codeHtml)}
318+
`;
295319
if (needUnExpand) {
296320
cacheCode += `<div class="cherry-mask-code-block">
297-
<div class="expand-btn ">
298-
<i class="ch-icon ch-icon-expand"></i>
299-
</div>
300-
</div>`;
321+
<div class="expand-btn ">
322+
<i class="ch-icon ch-icon-expand"></i>
323+
</div>
324+
</div>`;
301325
}
302326
cacheCode += '</div>';
303327
return cacheCode;

packages/cherry-markdown/src/core/hooks/Suggester.js

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -699,30 +699,6 @@ class SuggesterPanel {
699699
onCodeMirrorChange(codemirror, evt) {
700700
const { text, from, to, origin } = evt;
701701
const changeValue = text.length === 1 ? text[0] : '';
702-
// 【新增】处理 ``` 关键字的回车键特殊逻辑
703-
if (origin === '+input' ) {
704-
const cursor = codemirror.getCursor();
705-
const lineContent = codemirror.getLine(cursor.line - 1); // 上一行内容
706-
707-
// 检查是否以 ``` 开头(代码块开始标记)
708-
if (/^[\s]*```/.test(lineContent)) {
709-
710-
const language = lineContent.replace(/^[\s]*```\s*/, ''); // 提取语言标识符
711-
const cursorFrom = { line: 0, ch: 0 }; // 需要计算实际的起始位置
712-
713-
// 构建完整的代码块结构
714-
const codeBlock = `\`\`\`${language}\n\n\`\`\`\n`;
715-
716-
// 替换原来的 ``` 语言为完整的代码块
717-
codemirror.replaceRange(codeBlock, { line: cursor.line - 1, ch: 0 }, { line: cursor.line, ch: 0 });
718-
719-
// 将光标移动到代码块内部(第二行开始位置)
720-
codemirror.setCursor(cursor.line, language.length + 4);
721-
722-
this.stopRelate(); // 停止联想
723-
return; // 不继续执行后续逻辑
724-
}
725-
}
726702
// 首次输入命中关键词的时候开启联想
727703
if (!this.enableRelate() && this.suggesterConfig[changeValue]) {
728704
this.startRelate(codemirror, changeValue, from);
@@ -753,6 +729,31 @@ class SuggesterPanel {
753729
});
754730
}
755731
}
732+
// 处理 ``` 关键字的回车键特殊逻辑
733+
if (origin === '+input' && changeValue === '' ) {
734+
const cursor = codemirror.getCursor();
735+
const lineContent = codemirror.getLine(cursor.line - 1); // 上一行内容
736+
const nextLineContent = codemirror.getLine(cursor.line + 1); // 当前行(也就是按回车后新行的内容)
737+
738+
// Logger.debug('[CodeBlock]', '[onCodeMirrorChange]', 'lineContent', lineContent)
739+
// Logger.debug('[CodeBlock]', '[onCodeMirrorChange]', 'nextLineContent', nextLineContent)
740+
// 这样可以避免在已展开的代码块内部重复触发
741+
//如果相隔超过一行就不要管了,继续重新生成
742+
const backtickMatch = lineContent.match(/^[\s]*(```+)/);
743+
if (/^[\s]*```[^\s]/.test(lineContent) && !/^[\s]*```\s*$/.test(nextLineContent)) {
744+
const language = lineContent.replace(/^[\s]*```\s*/, '');
745+
const backticks = backtickMatch[1]; // 获取实际输入的 ``` 数量,如 ``` 或 ````
746+
747+
const codeBlock = `\`\`\`${language}\n\n${backticks}`;
748+
codemirror.replaceRange(codeBlock, { line: cursor.line - 1, ch: 0 }, { line: cursor.line, ch: 0 });
749+
750+
// 将光标移动到代码块内部
751+
codemirror.setCursor(cursor.line, language.length + 4);
752+
753+
this.stopRelate();
754+
return;
755+
}
756+
}
756757
}
757758

758759
/**

0 commit comments

Comments
 (0)