✨ Added markdown and mermaid preview
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
import { useEditorStore } from '@/stores/editorStore';
|
||||
|
||||
/**
|
||||
@@ -8,7 +7,6 @@ import { useEditorStore } from '@/stores/editorStore';
|
||||
export function createContentChangePlugin() {
|
||||
return ViewPlugin.fromClass(
|
||||
class ContentChangePlugin {
|
||||
private documentStore = useDocumentStore();
|
||||
private editorStore = useEditorStore();
|
||||
private lastContent = '';
|
||||
|
||||
@@ -24,11 +22,8 @@ export function createContentChangePlugin() {
|
||||
|
||||
this.lastContent = newContent;
|
||||
|
||||
// 通知编辑器管理器内容已变化
|
||||
const currentDocId = this.documentStore.currentDocumentId;
|
||||
if (currentDocId) {
|
||||
this.editorStore.onContentChange(currentDocId);
|
||||
}
|
||||
this.editorStore.onContentChange();
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
@@ -6,6 +6,34 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import { blockState } from './state';
|
||||
import { Block } from './types';
|
||||
|
||||
/**
|
||||
* 二分查找:找到包含指定位置的块
|
||||
* blocks 数组按位置排序,使用二分查找 O(log n)
|
||||
*/
|
||||
function findBlockAtPos(blocks: Block[], pos: number): Block | null {
|
||||
let left = 0;
|
||||
let right = blocks.length - 1;
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
const block = blocks[mid];
|
||||
|
||||
if (pos < block.range.from) {
|
||||
// 位置在当前块之前
|
||||
right = mid - 1;
|
||||
} else if (pos > block.range.to) {
|
||||
// 位置在当前块之后
|
||||
left = mid + 1;
|
||||
} else {
|
||||
// 位置在当前块范围内
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查位置是否在分隔符区域内
|
||||
@@ -13,14 +41,13 @@ import { blockState } from './state';
|
||||
function isInDelimiter(view: EditorView, pos: number): boolean {
|
||||
try {
|
||||
const blocks = view.state.field(blockState, false);
|
||||
if (!blocks) return false;
|
||||
if (!blocks || blocks.length === 0) return false;
|
||||
|
||||
for (const block of blocks) {
|
||||
if (pos >= block.delimiter.from && pos < block.delimiter.to) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
const block = findBlockAtPos(blocks, pos);
|
||||
if (!block) return false;
|
||||
|
||||
// 检查是否在该块的分隔符区域内
|
||||
return pos >= block.delimiter.from && pos < block.delimiter.to;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -34,22 +61,23 @@ function adjustPosition(view: EditorView, pos: number, forward: boolean): number
|
||||
const blocks = view.state.field(blockState, false);
|
||||
if (!blocks || blocks.length === 0) return pos;
|
||||
|
||||
for (const block of blocks) {
|
||||
// 如果位置在分隔符内
|
||||
if (pos >= block.delimiter.from && pos < block.delimiter.to) {
|
||||
// 向前移动:跳到该块内容的开始
|
||||
// 向后移动:跳到前一个块的内容末尾
|
||||
if (forward) {
|
||||
return block.content.from;
|
||||
} else {
|
||||
// 找到前一个块
|
||||
const blockIndex = blocks.indexOf(block);
|
||||
if (blockIndex > 0) {
|
||||
const prevBlock = blocks[blockIndex - 1];
|
||||
return prevBlock.content.to;
|
||||
}
|
||||
return block.delimiter.from;
|
||||
const block = findBlockAtPos(blocks, pos);
|
||||
if (!block) return pos;
|
||||
|
||||
// 如果位置在分隔符内
|
||||
if (pos >= block.delimiter.from && pos < block.delimiter.to) {
|
||||
// 向前移动:跳到该块内容的开始
|
||||
// 向后移动:跳到前一个块的内容末尾
|
||||
if (forward) {
|
||||
return block.content.from;
|
||||
} else {
|
||||
// 找到前一个块的索引
|
||||
const blockIndex = blocks.indexOf(block);
|
||||
if (blockIndex > 0) {
|
||||
const prevBlock = blocks[blockIndex - 1];
|
||||
return prevBlock.content.to;
|
||||
}
|
||||
return block.delimiter.from;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Markdown 预览扩展主入口
|
||||
*/
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { useThemeStore } from "@/stores/themeStore";
|
||||
import { usePanelStore } from "@/stores/panelStore";
|
||||
import { useDocumentStore } from "@/stores/documentStore";
|
||||
import { getActiveNoteBlock } from "../codeblock/state";
|
||||
import { createMarkdownPreviewTheme } from "./styles";
|
||||
import { previewPanelState, previewPanelPlugin, togglePreview, closePreviewWithAnimation } from "./state";
|
||||
|
||||
/**
|
||||
* 切换预览面板的命令
|
||||
*/
|
||||
export function toggleMarkdownPreview(view: EditorView): boolean {
|
||||
const panelStore = usePanelStore();
|
||||
const documentStore = useDocumentStore();
|
||||
const currentState = view.state.field(previewPanelState, false);
|
||||
const activeBlock = getActiveNoteBlock(view.state as any);
|
||||
|
||||
// 如果当前没有激活的 Markdown 块,不执行操作
|
||||
if (!activeBlock || activeBlock.language.name.toLowerCase() !== 'md') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取当前文档ID
|
||||
const currentDocumentId = documentStore.currentDocumentId;
|
||||
if (currentDocumentId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果预览面板已打开(无论预览的是不是当前块),关闭预览
|
||||
if (panelStore.markdownPreview.isOpen && !panelStore.markdownPreview.isClosing) {
|
||||
// 使用带动画的关闭函数
|
||||
closePreviewWithAnimation(view);
|
||||
} else {
|
||||
// 否则,打开当前块的预览
|
||||
view.dispatch({
|
||||
effects: togglePreview.of({
|
||||
documentId: currentDocumentId,
|
||||
blockFrom: activeBlock.content.from,
|
||||
blockTo: activeBlock.content.to
|
||||
})
|
||||
});
|
||||
|
||||
// 注意:store 状态由 ViewPlugin 在面板创建成功后更新
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 Markdown 预览扩展
|
||||
*/
|
||||
export function markdownPreviewExtension() {
|
||||
const themeStore = useThemeStore();
|
||||
const colors = themeStore.currentColors;
|
||||
|
||||
const theme = colors ? createMarkdownPreviewTheme(colors) : EditorView.baseTheme({});
|
||||
|
||||
return [previewPanelState, previewPanelPlugin, theme];
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Markdown 渲染器配置和自定义插件
|
||||
*/
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import {tasklist} from "@mdit/plugin-tasklist";
|
||||
import {katex} from "@mdit/plugin-katex";
|
||||
import markPlugin from "@/common/markdown-it/plugins/markdown-it-mark";
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import {full as emoji} from '@/common/markdown-it/plugins/markdown-it-emoji/'
|
||||
import footnote_plugin from "@/common/markdown-it/plugins/markdown-it-footnote"
|
||||
import sup_plugin from "@/common/markdown-it/plugins/markdown-it-sup"
|
||||
import ins_plugin from "@/common/markdown-it/plugins/markdown-it-ins"
|
||||
import deflist_plugin from "@/common/markdown-it/plugins/markdown-it-deflist"
|
||||
import abbr_plugin from "@/common/markdown-it/plugins/markdown-it-abbr"
|
||||
import sub_plugin from "@/common/markdown-it/plugins/markdown-it-sub"
|
||||
import {MermaidIt} from "@/common/markdown-it/plugins/markdown-it-mermaid"
|
||||
import {useThemeStore} from '@/stores/themeStore'
|
||||
|
||||
/**
|
||||
* 自定义链接插件:使用 data-href 替代 href,配合事件委托实现自定义跳转
|
||||
*/
|
||||
export function customLinkPlugin(md: MarkdownIt) {
|
||||
// 保存默认的 link_open 渲染器
|
||||
const defaultRender = md.renderer.rules.link_open || function (tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
// 重写 link_open 渲染器
|
||||
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
||||
const token = tokens[idx];
|
||||
|
||||
// 获取 href 属性
|
||||
const hrefIndex = token.attrIndex('href');
|
||||
if (hrefIndex >= 0) {
|
||||
const href = token.attrs![hrefIndex][1];
|
||||
|
||||
// 添加 data-href 属性保存原始链接
|
||||
token.attrPush(['data-href', href]);
|
||||
|
||||
// 添加 class 用于样式
|
||||
const classIndex = token.attrIndex('class');
|
||||
if (classIndex < 0) {
|
||||
token.attrPush(['class', 'markdown-link']);
|
||||
} else {
|
||||
token.attrs![classIndex][1] += ' markdown-link';
|
||||
}
|
||||
|
||||
// 移除 href 属性,防止默认跳转
|
||||
token.attrs!.splice(hrefIndex, 1);
|
||||
}
|
||||
|
||||
return defaultRender(tokens, idx, options, env, self);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Markdown-It 实例
|
||||
*/
|
||||
export function createMarkdownRenderer(): MarkdownIt {
|
||||
const themeStore = useThemeStore();
|
||||
const mermaidTheme = themeStore.isDarkMode ? "dark" : "default";
|
||||
|
||||
return new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true,
|
||||
langPrefix: "language-",
|
||||
highlight: (code, lang) => {
|
||||
// 对于大代码块(>1000行),跳过高亮以提升性能
|
||||
if (code.length > 50000) {
|
||||
return `<pre><code>${code}</code></pre>`;
|
||||
}
|
||||
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(code, {language: lang, ignoreIllegals: true}).value;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to highlight code block with language: ${lang}`, error);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
// 对于中等大小的代码块(>5000字符),跳过自动检测
|
||||
if (code.length > 5000) {
|
||||
return code;
|
||||
}
|
||||
|
||||
// 小代码块才使用自动检测
|
||||
try {
|
||||
return hljs.highlightAuto(code).value;
|
||||
} catch (error) {
|
||||
console.warn('Failed to auto-highlight code block', error);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
})
|
||||
.use(tasklist, {
|
||||
disabled: false,
|
||||
})
|
||||
.use(customLinkPlugin)
|
||||
.use(markPlugin)
|
||||
.use(emoji)
|
||||
.use(footnote_plugin)
|
||||
.use(sup_plugin)
|
||||
.use(ins_plugin)
|
||||
.use(deflist_plugin)
|
||||
.use(abbr_plugin)
|
||||
.use(sub_plugin)
|
||||
.use(katex)
|
||||
.use(MermaidIt, {
|
||||
theme: mermaidTheme
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
373
frontend/src/views/editor/extensions/markdownPreview/panel.ts
Normal file
373
frontend/src/views/editor/extensions/markdownPreview/panel.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* Markdown 预览面板 UI 组件
|
||||
*/
|
||||
import {EditorView, Panel, ViewUpdate} from "@codemirror/view";
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import * as runtime from "@wailsio/runtime";
|
||||
import {previewPanelState} from "./state";
|
||||
import {createMarkdownRenderer} from "./markdownRenderer";
|
||||
import {updateMermaidTheme} from "@/common/markdown-it/plugins/markdown-it-mermaid";
|
||||
import {useThemeStore} from "@/stores/themeStore";
|
||||
import {watch} from "vue";
|
||||
import {createDebounce} from "@/common/utils/debounce";
|
||||
import {morphHTML} from "@/common/utils/domDiff";
|
||||
|
||||
/**
|
||||
* Markdown 预览面板类
|
||||
*/
|
||||
export class MarkdownPreviewPanel {
|
||||
private md: MarkdownIt;
|
||||
private readonly dom: HTMLDivElement;
|
||||
private readonly resizeHandle: HTMLDivElement;
|
||||
private readonly content: HTMLDivElement;
|
||||
private view: EditorView;
|
||||
private themeUnwatch?: () => void;
|
||||
private lastRenderedContent: string = "";
|
||||
private debouncedUpdate: ReturnType<typeof createDebounce>;
|
||||
private isDestroyed: boolean = false; // 标记面板是否已销毁
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.view = view;
|
||||
this.md = createMarkdownRenderer();
|
||||
|
||||
// 创建防抖更新函数
|
||||
this.debouncedUpdate = createDebounce(() => {
|
||||
this.updateContentInternal();
|
||||
}, { delay: 1000 });
|
||||
|
||||
// 监听主题变化
|
||||
const themeStore = useThemeStore();
|
||||
this.themeUnwatch = watch(() => themeStore.isDarkMode, (isDark) => {
|
||||
const newTheme = isDark ? "dark" : "default";
|
||||
updateMermaidTheme(newTheme);
|
||||
this.lastRenderedContent = ""; // 清空缓存,强制重新渲染
|
||||
this.updateContentInternal();
|
||||
});
|
||||
|
||||
// 创建 DOM 结构
|
||||
this.dom = document.createElement("div");
|
||||
this.dom.className = "cm-markdown-preview-panel";
|
||||
|
||||
this.resizeHandle = document.createElement("div");
|
||||
this.resizeHandle.className = "cm-preview-resize-handle";
|
||||
|
||||
this.content = document.createElement("div");
|
||||
this.content.className = "cm-preview-content";
|
||||
|
||||
this.dom.appendChild(this.resizeHandle);
|
||||
this.dom.appendChild(this.content);
|
||||
|
||||
// 设置默认高度为编辑器高度的一半
|
||||
const defaultHeight = Math.floor(this.view.dom.clientHeight / 2);
|
||||
this.dom.style.height = `${defaultHeight}px`;
|
||||
|
||||
// 初始化拖动功能
|
||||
this.initResize();
|
||||
|
||||
// 初始化链接点击处理
|
||||
this.initLinkHandler();
|
||||
|
||||
// 初始渲染
|
||||
this.updateContentInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化链接点击处理(事件委托)
|
||||
*/
|
||||
private initLinkHandler(): void {
|
||||
this.content.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// 查找最近的 <a> 标签
|
||||
let linkElement = target;
|
||||
while (linkElement && linkElement !== this.content) {
|
||||
if (linkElement.tagName === 'A') {
|
||||
const anchor = linkElement as HTMLAnchorElement;
|
||||
const href = anchor.getAttribute('href');
|
||||
|
||||
// 处理脚注内部锚点链接
|
||||
if (href && href.startsWith('#')) {
|
||||
e.preventDefault();
|
||||
|
||||
// 在预览面板内查找目标元素
|
||||
const targetId = href.substring(1);
|
||||
|
||||
// 使用 getElementById 而不是 querySelector,因为 ID 可能包含特殊字符(如冒号)
|
||||
const targetElement = document.getElementById(targetId);
|
||||
|
||||
if (targetElement && this.content.contains(targetElement)) {
|
||||
// 平滑滚动到目标元素
|
||||
targetElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理带 data-href 的外部链接
|
||||
if (anchor.hasAttribute('data-href')) {
|
||||
e.preventDefault();
|
||||
const url = anchor.getAttribute('data-href');
|
||||
if (url && this.isValidUrl(url)) {
|
||||
runtime.Browser.OpenURL(url);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理其他链接
|
||||
if (href && !href.startsWith('#')) {
|
||||
e.preventDefault();
|
||||
|
||||
// 只有有效的 URL(http/https/mailto/file 等)才用浏览器打开
|
||||
if (this.isValidUrl(href)) {
|
||||
runtime.Browser.OpenURL(href);
|
||||
} else {
|
||||
// 相对路径或无效链接,显示提示
|
||||
console.warn('Invalid or relative link in preview:', href);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
linkElement = linkElement.parentElement as HTMLElement;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是有效的 URL(包含协议)
|
||||
*/
|
||||
private isValidUrl(url: string): boolean {
|
||||
try {
|
||||
// 检查是否包含协议
|
||||
if (url.match(/^[a-zA-Z][a-zA-Z\d+\-.]*:/)) {
|
||||
const parsedUrl = new URL(url);
|
||||
// 允许的协议列表
|
||||
const allowedProtocols = ['http:', 'https:', 'mailto:', 'file:', 'ftp:'];
|
||||
return allowedProtocols.includes(parsedUrl.protocol);
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化拖动调整高度功能
|
||||
*/
|
||||
private initResize(): void {
|
||||
let startY = 0;
|
||||
let startHeight = 0;
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const delta = startY - e.clientY;
|
||||
const maxHeight = this.getMaxHeight();
|
||||
const newHeight = Math.max(100, Math.min(maxHeight, startHeight + delta));
|
||||
this.dom.style.height = `${newHeight}px`;
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
this.resizeHandle.classList.remove("dragging");
|
||||
// 恢复 body 样式
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
|
||||
this.resizeHandle.addEventListener("mousedown", (e) => {
|
||||
e.preventDefault();
|
||||
startY = e.clientY;
|
||||
startHeight = this.dom.offsetHeight;
|
||||
this.resizeHandle.classList.add("dragging");
|
||||
// 设置 body 样式,防止拖动时光标闪烁
|
||||
document.body.style.cursor = "ns-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态计算最大高度(编辑器高度)
|
||||
*/
|
||||
private getMaxHeight(): number {
|
||||
return this.view.dom.clientHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部更新预览内容(带缓存 + DOM Diff 优化)
|
||||
*/
|
||||
private updateContentInternal(): void {
|
||||
// 如果面板已销毁,直接返回
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const state = this.view.state;
|
||||
const currentPreviewState = state.field(previewPanelState, false);
|
||||
|
||||
if (!currentPreviewState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blockContent = state.doc.sliceString(
|
||||
currentPreviewState.blockFrom,
|
||||
currentPreviewState.blockTo
|
||||
);
|
||||
|
||||
if (!blockContent || blockContent.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 缓存检查:如果内容没变,不重新渲染
|
||||
if (blockContent === this.lastRenderedContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于大内容,使用异步渲染避免阻塞主线程
|
||||
if (blockContent.length > 1000) {
|
||||
this.renderLargeContentAsync(blockContent);
|
||||
} else {
|
||||
// 小内容使用 DOM Diff 优化渲染
|
||||
this.renderWithDiff(blockContent);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn("Error updating preview content:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 DOM Diff 渲染内容(保留未变化的节点)
|
||||
*/
|
||||
private renderWithDiff(content: string): void {
|
||||
// 如果面板已销毁,直接返回
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newHtml = this.md.render(content);
|
||||
|
||||
// 如果是首次渲染或内容为空,直接设置 innerHTML
|
||||
if (!this.lastRenderedContent || this.content.children.length === 0) {
|
||||
this.content.innerHTML = newHtml;
|
||||
} else {
|
||||
// 使用 DOM Diff 增量更新
|
||||
morphHTML(this.content, newHtml);
|
||||
}
|
||||
|
||||
this.lastRenderedContent = content;
|
||||
} catch (error) {
|
||||
console.warn("Error rendering with diff:", error);
|
||||
// 降级到直接设置 innerHTML
|
||||
if (!this.isDestroyed) {
|
||||
this.content.innerHTML = this.md.render(content);
|
||||
this.lastRenderedContent = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步渲染大内容(使用 DOM Diff 优化)
|
||||
*/
|
||||
private renderLargeContentAsync(content: string): void {
|
||||
// 如果面板已销毁,直接返回
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是首次渲染,显示加载状态
|
||||
if (!this.lastRenderedContent) {
|
||||
this.content.innerHTML = '<div class="markdown-loading">Rendering...</div>';
|
||||
}
|
||||
|
||||
// 使用 requestIdleCallback 在浏览器空闲时渲染
|
||||
const callback = window.requestIdleCallback || ((cb: IdleRequestCallback) => setTimeout(cb, 1));
|
||||
|
||||
callback(() => {
|
||||
// 再次检查是否已销毁(异步回调时可能已经关闭)
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const html = this.md.render(content);
|
||||
|
||||
// 如果是首次渲染或之前内容为空,直接设置
|
||||
if (!this.lastRenderedContent || this.content.children.length === 0) {
|
||||
// 使用 DocumentFragment 减少 DOM 操作
|
||||
const fragment = document.createRange().createContextualFragment(html);
|
||||
this.content.innerHTML = '';
|
||||
this.content.appendChild(fragment);
|
||||
} else {
|
||||
// 使用 DOM Diff 增量更新(保留滚动位置和未变化的节点)
|
||||
morphHTML(this.content, html);
|
||||
}
|
||||
|
||||
this.lastRenderedContent = content;
|
||||
} catch (error) {
|
||||
console.warn("Error rendering large content:", error);
|
||||
if (!this.isDestroyed) {
|
||||
this.content.innerHTML = '<div class="markdown-error">Render failed</div>';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应编辑器更新
|
||||
*/
|
||||
public update(update: ViewUpdate): void {
|
||||
if (update.docChanged) {
|
||||
// 文档改变时使用防抖更新
|
||||
this.debouncedUpdate.debouncedFn();
|
||||
} else if (update.selectionSet) {
|
||||
// 光标移动时不触发更新
|
||||
// 如果需要根据光标位置更新,可以在这里处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
public destroy(): void {
|
||||
// 标记为已销毁,防止异步回调继续执行
|
||||
this.isDestroyed = true;
|
||||
|
||||
// 清理防抖
|
||||
if (this.debouncedUpdate) {
|
||||
this.debouncedUpdate.cancel();
|
||||
}
|
||||
|
||||
// 清理主题监听
|
||||
if (this.themeUnwatch) {
|
||||
this.themeUnwatch();
|
||||
this.themeUnwatch = undefined;
|
||||
}
|
||||
|
||||
// 清空缓存
|
||||
this.lastRenderedContent = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 CodeMirror Panel 对象
|
||||
*/
|
||||
public getPanel(): Panel {
|
||||
return {
|
||||
top: false,
|
||||
dom: this.dom,
|
||||
update: (update: ViewUpdate) => this.update(update),
|
||||
destroy: () => this.destroy()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建预览面板
|
||||
*/
|
||||
export function createPreviewPanel(view: EditorView): Panel {
|
||||
const panel = new MarkdownPreviewPanel(view);
|
||||
return panel.getPanel();
|
||||
}
|
||||
|
||||
142
frontend/src/views/editor/extensions/markdownPreview/state.ts
Normal file
142
frontend/src/views/editor/extensions/markdownPreview/state.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Markdown 预览面板的 CodeMirror 状态管理
|
||||
*/
|
||||
import { EditorView, showPanel, ViewUpdate, ViewPlugin } from "@codemirror/view";
|
||||
import { StateEffect, StateField } from "@codemirror/state";
|
||||
import { getActiveNoteBlock } from "../codeblock/state";
|
||||
import { usePanelStore } from "@/stores/panelStore";
|
||||
import { createPreviewPanel } from "./panel";
|
||||
import type { PreviewState } from "./types";
|
||||
|
||||
/**
|
||||
* 定义切换预览面板的 Effect
|
||||
*/
|
||||
export const togglePreview = StateEffect.define<PreviewState | null>();
|
||||
|
||||
/**
|
||||
* 关闭面板(带动画)
|
||||
*/
|
||||
export function closePreviewWithAnimation(view: EditorView): void {
|
||||
const panelStore = usePanelStore();
|
||||
|
||||
// 标记开始关闭
|
||||
panelStore.startClosingMarkdownPreview();
|
||||
|
||||
const panelElement = view.dom.querySelector('.cm-panels.cm-panels-bottom') as HTMLElement;
|
||||
if (panelElement) {
|
||||
panelElement.style.animation = 'panelSlideDown 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||
// 等待动画完成后再关闭面板
|
||||
setTimeout(() => {
|
||||
view.dispatch({
|
||||
effects: togglePreview.of(null)
|
||||
});
|
||||
panelStore.closeMarkdownPreview();
|
||||
}, 280);
|
||||
} else {
|
||||
view.dispatch({
|
||||
effects: togglePreview.of(null)
|
||||
});
|
||||
panelStore.closeMarkdownPreview();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义预览面板的状态字段
|
||||
*/
|
||||
export const previewPanelState = StateField.define<PreviewState | null>({
|
||||
create: () => null,
|
||||
update(value, tr) {
|
||||
const panelStore = usePanelStore();
|
||||
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(togglePreview)) {
|
||||
value = e.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有预览状态,智能管理预览生命周期
|
||||
if (value && !value.closing) {
|
||||
const activeBlock = getActiveNoteBlock(tr.state as any);
|
||||
|
||||
// 关键修复:检查预览状态是否属于当前文档
|
||||
// 如果 panelStore 中没有当前文档的预览状态(说明切换了文档),
|
||||
// 则不执行关闭逻辑,保持其他文档的预览状态
|
||||
if (!panelStore.markdownPreview.isOpen) {
|
||||
// 当前文档没有预览,不处理
|
||||
return value;
|
||||
}
|
||||
|
||||
// 场景1:离开 Markdown 块或无激活块 → 关闭预览
|
||||
if (!activeBlock || activeBlock.language.name.toLowerCase() !== 'md') {
|
||||
if (!panelStore.markdownPreview.isClosing) {
|
||||
return { ...value, closing: true };
|
||||
}
|
||||
}
|
||||
// 场景2:切换到其他块(起始位置变化)→ 关闭预览
|
||||
else if (activeBlock.content.from !== value.blockFrom) {
|
||||
if (!panelStore.markdownPreview.isClosing) {
|
||||
return { ...value, closing: true };
|
||||
}
|
||||
}
|
||||
// 场景3:还在同一个块内编辑(只有结束位置变化)→ 更新范围,实时预览
|
||||
else if (activeBlock.content.to !== value.blockTo) {
|
||||
// 更新 panelStore 中的预览范围
|
||||
panelStore.updatePreviewRange(value.blockFrom, activeBlock.content.to);
|
||||
|
||||
return {
|
||||
documentId: value.documentId,
|
||||
blockFrom: value.blockFrom,
|
||||
blockTo: activeBlock.content.to,
|
||||
closing: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
provide: f => showPanel.from(f, state => state ? createPreviewPanel : null)
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建监听插件
|
||||
*/
|
||||
export const previewPanelPlugin = ViewPlugin.fromClass(class {
|
||||
private lastState: PreviewState | null | undefined = null;
|
||||
private panelStore = usePanelStore();
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
this.lastState = view.state.field(previewPanelState, false);
|
||||
this.panelStore.setEditorView(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
const currentState = update.state.field(previewPanelState, false);
|
||||
|
||||
// 检测到面板打开(从 null 变为有值,且不是 closing)
|
||||
if (currentState && !currentState.closing && !this.lastState) {
|
||||
// 验证面板 DOM 是否真正创建成功
|
||||
requestAnimationFrame(() => {
|
||||
const panelElement = this.view.dom.querySelector('.cm-markdown-preview-panel');
|
||||
if (panelElement) {
|
||||
// 面板创建成功,更新 store 状态
|
||||
this.panelStore.openMarkdownPreview(currentState.blockFrom, currentState.blockTo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检测到状态变为 closing
|
||||
if (currentState?.closing && !this.lastState?.closing) {
|
||||
// 触发关闭动画
|
||||
closePreviewWithAnimation(this.view);
|
||||
}
|
||||
|
||||
this.lastState = currentState;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 不调用 reset(),因为那会清空所有文档的预览状态
|
||||
// 只清理编辑器视图引用
|
||||
this.panelStore.setEditorView(null);
|
||||
}
|
||||
});
|
||||
|
||||
356
frontend/src/views/editor/extensions/markdownPreview/styles.ts
Normal file
356
frontend/src/views/editor/extensions/markdownPreview/styles.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import type { ThemeColors } from "@/views/editor/theme/types";
|
||||
|
||||
/**
|
||||
* 创建 Markdown 预览面板的主题样式
|
||||
*/
|
||||
export function createMarkdownPreviewTheme(colors: ThemeColors) {
|
||||
// GitHub 官方颜色变量
|
||||
const isDark = colors.dark;
|
||||
|
||||
// GitHub Light 主题颜色
|
||||
const lightColors = {
|
||||
fg: {
|
||||
default: "#1F2328",
|
||||
muted: "#656d76",
|
||||
subtle: "#6e7781"
|
||||
},
|
||||
border: {
|
||||
default: "#d0d7de",
|
||||
muted: "#d8dee4"
|
||||
},
|
||||
canvas: {
|
||||
default: "#ffffff",
|
||||
subtle: "#f6f8fa"
|
||||
},
|
||||
accent: {
|
||||
fg: "#0969da",
|
||||
emphasis: "#0969da"
|
||||
}
|
||||
};
|
||||
|
||||
// GitHub Dark 主题颜色
|
||||
const darkColors = {
|
||||
fg: {
|
||||
default: "#e6edf3",
|
||||
muted: "#7d8590",
|
||||
subtle: "#6e7681"
|
||||
},
|
||||
border: {
|
||||
default: "#30363d",
|
||||
muted: "#21262d"
|
||||
},
|
||||
canvas: {
|
||||
default: "#0d1117",
|
||||
subtle: "#161b22"
|
||||
},
|
||||
accent: {
|
||||
fg: "#2f81f7",
|
||||
emphasis: "#2f81f7"
|
||||
}
|
||||
};
|
||||
|
||||
const ghColors = isDark ? darkColors : lightColors;
|
||||
|
||||
return EditorView.theme({
|
||||
// 面板容器
|
||||
".cm-markdown-preview-panel": {
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden"
|
||||
},
|
||||
|
||||
// 拖动调整大小的手柄
|
||||
".cm-preview-resize-handle": {
|
||||
width: "100%",
|
||||
height: "3px",
|
||||
backgroundColor: colors.borderColor,
|
||||
cursor: "ns-resize",
|
||||
position: "relative",
|
||||
flexShrink: 0,
|
||||
transition: "background-color 0.2s ease",
|
||||
"&:hover": {
|
||||
backgroundColor: colors.selection
|
||||
},
|
||||
"&.dragging": {
|
||||
backgroundColor: colors.selection
|
||||
}
|
||||
},
|
||||
|
||||
// 内容区域
|
||||
".cm-preview-content": {
|
||||
flex: 1,
|
||||
padding: "45px",
|
||||
overflow: "auto",
|
||||
fontSize: "16px",
|
||||
lineHeight: "1.5",
|
||||
color: ghColors.fg.default,
|
||||
wordWrap: "break-word",
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'",
|
||||
boxSizing: "border-box",
|
||||
|
||||
// Loading state
|
||||
"& .markdown-loading, & .markdown-error": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: "200px",
|
||||
fontSize: "14px",
|
||||
color: ghColors.fg.muted
|
||||
},
|
||||
|
||||
"& .markdown-error": {
|
||||
color: "#f85149"
|
||||
},
|
||||
|
||||
// ========== 标题样式 ==========
|
||||
"& h1, & h2, & h3, & h4, & h5, & h6": {
|
||||
marginTop: "24px",
|
||||
marginBottom: "16px",
|
||||
fontWeight: "600",
|
||||
lineHeight: "1.25",
|
||||
color: ghColors.fg.default
|
||||
},
|
||||
"& h1": {
|
||||
fontSize: "2em",
|
||||
borderBottom: `1px solid ${ghColors.border.muted}`,
|
||||
paddingBottom: "0.3em"
|
||||
},
|
||||
"& h2": {
|
||||
fontSize: "1.5em",
|
||||
borderBottom: `1px solid ${ghColors.border.muted}`,
|
||||
paddingBottom: "0.3em"
|
||||
},
|
||||
"& h3": {
|
||||
fontSize: "1.25em"
|
||||
},
|
||||
"& h4": {
|
||||
fontSize: "1em"
|
||||
},
|
||||
"& h5": {
|
||||
fontSize: "0.875em"
|
||||
},
|
||||
"& h6": {
|
||||
fontSize: "0.85em",
|
||||
color: ghColors.fg.muted
|
||||
},
|
||||
|
||||
// ========== 段落和文本 ==========
|
||||
"& p": {
|
||||
marginTop: "0",
|
||||
marginBottom: "16px"
|
||||
},
|
||||
"& strong": {
|
||||
fontWeight: "600"
|
||||
},
|
||||
"& em": {
|
||||
fontStyle: "italic"
|
||||
},
|
||||
"& del": {
|
||||
textDecoration: "line-through",
|
||||
opacity: "0.7"
|
||||
},
|
||||
|
||||
// ========== 列表 ==========
|
||||
"& ul, & ol": {
|
||||
paddingLeft: "2em",
|
||||
marginTop: "0",
|
||||
marginBottom: "16px"
|
||||
},
|
||||
"& ul ul, & ul ol, & ol ol, & ol ul": {
|
||||
marginTop: "0",
|
||||
marginBottom: "0"
|
||||
},
|
||||
"& li": {
|
||||
wordWrap: "break-all"
|
||||
},
|
||||
"& li > p": {
|
||||
marginTop: "16px"
|
||||
},
|
||||
"& li + li": {
|
||||
marginTop: "0.25em"
|
||||
},
|
||||
|
||||
// 任务列表
|
||||
"& .task-list-item": {
|
||||
listStyleType: "none",
|
||||
position: "relative",
|
||||
paddingLeft: "1.5em"
|
||||
},
|
||||
"& .task-list-item + .task-list-item": {
|
||||
marginTop: "3px"
|
||||
},
|
||||
"& .task-list-item input[type='checkbox']": {
|
||||
font: "inherit",
|
||||
overflow: "visible",
|
||||
fontFamily: "inherit",
|
||||
fontSize: "inherit",
|
||||
lineHeight: "inherit",
|
||||
boxSizing: "border-box",
|
||||
padding: "0",
|
||||
margin: "0 0.2em 0.25em -1.6em",
|
||||
verticalAlign: "middle",
|
||||
cursor: "pointer"
|
||||
},
|
||||
|
||||
// ========== 代码块 ==========
|
||||
"& code, & tt": {
|
||||
fontFamily: "SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace",
|
||||
fontSize: "85%",
|
||||
padding: "0.2em 0.4em",
|
||||
margin: "0",
|
||||
backgroundColor: isDark ? "rgba(110, 118, 129, 0.4)" : "rgba(27, 31, 35, 0.05)",
|
||||
borderRadius: "3px"
|
||||
},
|
||||
|
||||
"& pre": {
|
||||
position: "relative",
|
||||
backgroundColor: isDark ? "#161b22" : "#f6f8fa",
|
||||
padding: "40px 16px 16px 16px",
|
||||
borderRadius: "6px",
|
||||
overflow: "auto",
|
||||
margin: "16px 0",
|
||||
fontSize: "85%",
|
||||
lineHeight: "1.45",
|
||||
wordWrap: "normal",
|
||||
|
||||
// macOS 窗口样式 - 使用伪元素创建顶部栏
|
||||
"&::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
right: "0",
|
||||
height: "28px",
|
||||
backgroundColor: isDark ? "#1c1c1e" : "#e8e8e8",
|
||||
borderBottom: `1px solid ${ghColors.border.default}`,
|
||||
borderRadius: "6px 6px 0 0"
|
||||
},
|
||||
|
||||
// macOS 三个控制按钮
|
||||
"&::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: "10px",
|
||||
left: "12px",
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: isDark ? "#ec6a5f" : "#ff5f57",
|
||||
boxShadow: `
|
||||
18px 0 0 0 ${isDark ? "#f4bf4f" : "#febc2e"},
|
||||
36px 0 0 0 ${isDark ? "#61c554" : "#28c840"}
|
||||
`
|
||||
}
|
||||
},
|
||||
|
||||
"& pre code, & pre tt": {
|
||||
display: "inline",
|
||||
maxWidth: "auto",
|
||||
padding: "0",
|
||||
margin: "0",
|
||||
overflow: "visible",
|
||||
lineHeight: "inherit",
|
||||
wordWrap: "normal",
|
||||
backgroundColor: "transparent",
|
||||
border: "0",
|
||||
fontSize: "100%",
|
||||
color: ghColors.fg.default,
|
||||
wordBreak: "normal",
|
||||
whiteSpace: "pre"
|
||||
},
|
||||
|
||||
// ========== 引用块 ==========
|
||||
"& blockquote": {
|
||||
margin: "16px 0",
|
||||
padding: "0 1em",
|
||||
color: isDark ? "#7d8590" : "#6a737d",
|
||||
borderLeft: isDark ? "0.25em solid #3b434b" : "0.25em solid #dfe2e5"
|
||||
},
|
||||
"& blockquote > :first-child": {
|
||||
marginTop: "0"
|
||||
},
|
||||
"& blockquote > :last-child": {
|
||||
marginBottom: "0"
|
||||
},
|
||||
|
||||
// ========== 分割线 ==========
|
||||
"& hr": {
|
||||
height: "0.25em",
|
||||
padding: "0",
|
||||
margin: "24px 0",
|
||||
backgroundColor: isDark ? "#21262d" : "#e1e4e8",
|
||||
border: "0",
|
||||
overflow: "hidden",
|
||||
boxSizing: "content-box"
|
||||
},
|
||||
|
||||
// ========== 表格 ==========
|
||||
"& table": {
|
||||
borderSpacing: "0",
|
||||
borderCollapse: "collapse",
|
||||
display: "block",
|
||||
width: "100%",
|
||||
overflow: "auto",
|
||||
marginTop: "0",
|
||||
marginBottom: "16px"
|
||||
},
|
||||
"& table tr": {
|
||||
backgroundColor: isDark ? "#0d1117" : "#ffffff",
|
||||
borderTop: isDark ? "1px solid #21262d" : "1px solid #c6cbd1"
|
||||
},
|
||||
"& table th, & table td": {
|
||||
padding: "6px 13px",
|
||||
border: isDark ? "1px solid #30363d" : "1px solid #dfe2e5"
|
||||
},
|
||||
"& table th": {
|
||||
fontWeight: "600"
|
||||
},
|
||||
|
||||
// ========== 链接 ==========
|
||||
"& a, & .markdown-link": {
|
||||
color: isDark ? "#58a6ff" : "#0366d6",
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
textDecoration: "underline"
|
||||
}
|
||||
},
|
||||
|
||||
// ========== 图片 ==========
|
||||
"& img": {
|
||||
maxWidth: "100%",
|
||||
height: "auto",
|
||||
borderRadius: "4px",
|
||||
margin: "16px 0"
|
||||
},
|
||||
|
||||
// ========== 其他元素 ==========
|
||||
"& kbd": {
|
||||
display: "inline-block",
|
||||
padding: "3px 5px",
|
||||
fontSize: "11px",
|
||||
lineHeight: "10px",
|
||||
color: ghColors.fg.default,
|
||||
verticalAlign: "middle",
|
||||
backgroundColor: ghColors.canvas.subtle,
|
||||
border: `solid 1px ${isDark ? "rgba(110, 118, 129, 0.4)" : "rgba(175, 184, 193, 0.2)"}`,
|
||||
borderBottom: `solid 2px ${isDark ? "rgba(110, 118, 129, 0.4)" : "rgba(175, 184, 193, 0.2)"}`,
|
||||
borderRadius: "6px",
|
||||
boxShadow: "inset 0 -1px 0 rgba(175, 184, 193, 0.2)"
|
||||
},
|
||||
|
||||
// 首个子元素去除上边距
|
||||
"& > *:first-child": {
|
||||
marginTop: "0 !important"
|
||||
},
|
||||
|
||||
// 最后一个子元素去除下边距
|
||||
"& > *:last-child": {
|
||||
marginBottom: "0 !important"
|
||||
}
|
||||
}
|
||||
}, { dark: colors.dark });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Markdown 预览面板相关类型定义
|
||||
*/
|
||||
|
||||
// 预览面板状态
|
||||
export interface PreviewState {
|
||||
documentId: number; // 预览所属的文档ID
|
||||
blockFrom: number;
|
||||
blockTo: number;
|
||||
closing?: boolean; // 标记面板正在关闭
|
||||
}
|
||||
|
||||
@@ -10,280 +10,304 @@ import type {ThemeColors} from './types';
|
||||
* @returns CodeMirror Extension数组
|
||||
*/
|
||||
export function createBaseTheme(colors: ThemeColors): Extension {
|
||||
// 编辑器主题样式
|
||||
const theme = EditorView.theme({
|
||||
'&': {
|
||||
color: colors.foreground,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
// 编辑器主题样式
|
||||
const theme = EditorView.theme({
|
||||
'&': {
|
||||
color: colors.foreground,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
|
||||
// 确保编辑器容器背景一致
|
||||
'.cm-editor': {
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
// 确保编辑器容器背景一致
|
||||
'.cm-editor': {
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
|
||||
// 确保滚动区域背景一致
|
||||
'.cm-scroller': {
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
// 确保滚动区域背景一致
|
||||
'.cm-scroller': {
|
||||
backgroundColor: colors.background,
|
||||
transition: 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
},
|
||||
|
||||
// 编辑器内容
|
||||
'.cm-content': {
|
||||
caretColor: colors.cursor,
|
||||
paddingTop: '4px',
|
||||
},
|
||||
// 编辑器内容
|
||||
'.cm-content': {
|
||||
caretColor: colors.cursor,
|
||||
paddingTop: '4px',
|
||||
},
|
||||
|
||||
// 光标
|
||||
'.cm-cursor, .cm-dropCursor': {
|
||||
borderLeftColor: colors.cursor,
|
||||
borderLeftWidth: '2px',
|
||||
paddingTop: '4px',
|
||||
marginTop: '-2px',
|
||||
},
|
||||
// 光标
|
||||
'.cm-cursor, .cm-dropCursor': {
|
||||
borderLeftColor: colors.cursor,
|
||||
borderLeftWidth: '2px',
|
||||
paddingTop: '4px',
|
||||
marginTop: '-2px',
|
||||
},
|
||||
|
||||
// 选择
|
||||
'.cm-selectionBackground': {
|
||||
backgroundColor: colors.selectionBlur,
|
||||
},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
|
||||
backgroundColor: colors.selection,
|
||||
},
|
||||
'.cm-content ::selection': {
|
||||
backgroundColor: colors.selection,
|
||||
},
|
||||
'.cm-activeLine.code-empty-block-selected': {
|
||||
backgroundColor: colors.selection,
|
||||
},
|
||||
// 选择
|
||||
'.cm-selectionBackground': {
|
||||
backgroundColor: colors.selectionBlur,
|
||||
},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
|
||||
backgroundColor: colors.selection,
|
||||
},
|
||||
'.cm-content ::selection': {
|
||||
backgroundColor: colors.selection,
|
||||
},
|
||||
'.cm-activeLine.code-empty-block-selected': {
|
||||
backgroundColor: colors.selection,
|
||||
},
|
||||
|
||||
// 当前行高亮
|
||||
'.cm-activeLine': {
|
||||
backgroundColor: colors.activeLine
|
||||
},
|
||||
// 当前行高亮
|
||||
'.cm-activeLine': {
|
||||
backgroundColor: colors.activeLine
|
||||
},
|
||||
|
||||
// 行号区域
|
||||
'.cm-gutters': {
|
||||
backgroundColor: colors.dark ? 'rgba(0,0,0, 0.1)' : 'rgba(0,0,0, 0.04)',
|
||||
color: colors.lineNumber,
|
||||
border: 'none',
|
||||
borderRight: colors.dark ? 'none' : `1px solid ${colors.borderLight}`,
|
||||
padding: '0 2px 0 4px',
|
||||
userSelect: 'none',
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: 'transparent',
|
||||
color: colors.activeLineNumber,
|
||||
},
|
||||
// 行号区域
|
||||
'.cm-gutters': {
|
||||
backgroundColor: colors.dark ? 'rgba(0,0,0, 0.1)' : 'rgba(0,0,0, 0.04)',
|
||||
color: colors.lineNumber,
|
||||
border: 'none',
|
||||
borderRight: colors.dark ? 'none' : `1px solid ${colors.borderLight}`,
|
||||
padding: '0 2px 0 4px',
|
||||
userSelect: 'none',
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: 'transparent',
|
||||
color: colors.activeLineNumber,
|
||||
},
|
||||
|
||||
// 折叠功能
|
||||
'.cm-foldGutter': {
|
||||
marginLeft: '0px',
|
||||
},
|
||||
'.cm-foldGutter .cm-gutterElement': {
|
||||
opacity: 0,
|
||||
transition: 'opacity 400ms',
|
||||
},
|
||||
'.cm-gutters:hover .cm-gutterElement': {
|
||||
opacity: 1,
|
||||
},
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: colors.comment,
|
||||
},
|
||||
// 折叠功能
|
||||
'.cm-foldGutter': {
|
||||
marginLeft: '0px',
|
||||
},
|
||||
'.cm-foldGutter .cm-gutterElement': {
|
||||
opacity: 0,
|
||||
transition: 'opacity 400ms',
|
||||
},
|
||||
'.cm-gutters:hover .cm-gutterElement': {
|
||||
opacity: 1,
|
||||
},
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: colors.comment,
|
||||
},
|
||||
|
||||
// 面板
|
||||
'.cm-panels': {
|
||||
backgroundColor: colors.dropdownBackground,
|
||||
color: colors.foreground
|
||||
},
|
||||
'.cm-panels.cm-panels-top': {
|
||||
borderBottom: '2px solid black'
|
||||
},
|
||||
'.cm-panels.cm-panels-bottom': {
|
||||
borderTop: '2px solid black'
|
||||
},
|
||||
// 面板
|
||||
'.cm-panels': {
|
||||
// backgroundColor: colors.dropdownBackground,
|
||||
// color: colors.foreground
|
||||
},
|
||||
'.cm-panels.cm-panels-top': {
|
||||
borderBottom: '2px solid black'
|
||||
},
|
||||
'.cm-panels.cm-panels-bottom': {
|
||||
animation: 'panelSlideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
},
|
||||
'@keyframes panelSlideUp': {
|
||||
from: {
|
||||
transform: 'translateY(100%)',
|
||||
opacity: '0'
|
||||
},
|
||||
to: {
|
||||
transform: 'translateY(0)',
|
||||
opacity: '1'
|
||||
}
|
||||
},
|
||||
'@keyframes panelSlideDown': {
|
||||
from: {
|
||||
transform: 'translateY(0)',
|
||||
opacity: '1'
|
||||
},
|
||||
to: {
|
||||
transform: 'translateY(100%)',
|
||||
opacity: '0'
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索匹配
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: 'transparent',
|
||||
outline: `1px solid ${colors.searchMatch}`,
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: colors.searchMatch,
|
||||
color: colors.background,
|
||||
},
|
||||
'.cm-selectionMatch': {
|
||||
backgroundColor: colors.dark ? '#50606D' : '#e6f3ff',
|
||||
},
|
||||
// 搜索匹配
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: 'transparent',
|
||||
outline: `1px solid ${colors.searchMatch}`,
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: colors.searchMatch,
|
||||
color: colors.background,
|
||||
},
|
||||
'.cm-selectionMatch': {
|
||||
backgroundColor: colors.dark ? '#50606D' : '#e6f3ff',
|
||||
},
|
||||
|
||||
// 括号匹配
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
outline: `0.5px solid ${colors.searchMatch}`,
|
||||
},
|
||||
'&.cm-focused .cm-matchingBracket': {
|
||||
backgroundColor: colors.matchingBracket,
|
||||
color: 'inherit',
|
||||
},
|
||||
'&.cm-focused .cm-nonmatchingBracket': {
|
||||
outline: colors.dark ? '0.5px solid #bc8f8f' : '0.5px solid #d73a49',
|
||||
},
|
||||
// 括号匹配
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
outline: `0.5px solid ${colors.searchMatch}`,
|
||||
},
|
||||
'&.cm-focused .cm-matchingBracket': {
|
||||
backgroundColor: colors.matchingBracket,
|
||||
color: 'inherit',
|
||||
},
|
||||
'&.cm-focused .cm-nonmatchingBracket': {
|
||||
outline: colors.dark ? '0.5px solid #bc8f8f' : '0.5px solid #d73a49',
|
||||
},
|
||||
|
||||
// 编辑器焦点
|
||||
'&.cm-editor.cm-focused': {
|
||||
outline: 'none',
|
||||
},
|
||||
// 编辑器焦点
|
||||
'&.cm-editor.cm-focused': {
|
||||
outline: 'none',
|
||||
},
|
||||
|
||||
// 工具提示
|
||||
'.cm-tooltip': {
|
||||
border: colors.dark ? 'none' : `1px solid ${colors.dropdownBorder}`,
|
||||
backgroundColor: colors.surface,
|
||||
color: colors.foreground,
|
||||
boxShadow: colors.dark ? 'none' : '0 2px 8px rgba(0,0,0,0.1)',
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: colors.surface,
|
||||
borderBottomColor: colors.surface,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
backgroundColor: colors.activeLine,
|
||||
color: colors.foreground,
|
||||
},
|
||||
},
|
||||
// 工具提示
|
||||
'.cm-tooltip': {
|
||||
border: colors.dark ? 'none' : `1px solid ${colors.dropdownBorder}`,
|
||||
backgroundColor: colors.surface,
|
||||
color: colors.foreground,
|
||||
boxShadow: colors.dark ? 'none' : '0 2px 8px rgba(0,0,0,0.1)',
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: colors.surface,
|
||||
borderBottomColor: colors.surface,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
backgroundColor: colors.activeLine,
|
||||
color: colors.foreground,
|
||||
},
|
||||
},
|
||||
|
||||
// 代码块层(自定义)
|
||||
'.code-blocks-layer': {
|
||||
width: '100%',
|
||||
},
|
||||
'.code-blocks-layer .block-even, .code-blocks-layer .block-odd': {
|
||||
width: '100%',
|
||||
boxSizing: 'content-box',
|
||||
},
|
||||
'.code-blocks-layer .block-even': {
|
||||
background: colors.background,
|
||||
borderTop: `1px solid ${colors.borderColor}`,
|
||||
},
|
||||
'.code-blocks-layer .block-even:first-child': {
|
||||
borderTop: 'none',
|
||||
},
|
||||
'.code-blocks-layer .block-odd': {
|
||||
background: colors.backgroundSecondary,
|
||||
borderTop: `1px solid ${colors.borderColor}`,
|
||||
},
|
||||
// 代码块层(自定义)
|
||||
'.code-blocks-layer': {
|
||||
width: '100%',
|
||||
},
|
||||
'.code-blocks-layer .block-even, .code-blocks-layer .block-odd': {
|
||||
width: '100%',
|
||||
boxSizing: 'content-box',
|
||||
},
|
||||
'.code-blocks-layer .block-even': {
|
||||
background: colors.background,
|
||||
borderTop: `1px solid ${colors.borderColor}`,
|
||||
},
|
||||
'.code-blocks-layer .block-even:first-child': {
|
||||
borderTop: 'none',
|
||||
},
|
||||
'.code-blocks-layer .block-odd': {
|
||||
background: colors.backgroundSecondary,
|
||||
borderTop: `1px solid ${colors.borderColor}`,
|
||||
},
|
||||
|
||||
// 数学计算结果(自定义)
|
||||
'.code-blocks-math-result': {
|
||||
paddingLeft: "12px",
|
||||
position: "relative",
|
||||
},
|
||||
".code-blocks-math-result .inner": {
|
||||
background: colors.dark ? '#0e1217' : '#48b57e',
|
||||
color: colors.dark ? '#a0e7c7' : '#fff',
|
||||
padding: '0px 4px',
|
||||
borderRadius: '2px',
|
||||
boxShadow: colors.dark ? '0 0 3px rgba(0,0,0, 0.3)' : '0 0 3px rgba(0,0,0, 0.1)',
|
||||
cursor: 'pointer',
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
'.code-blocks-math-result-copied': {
|
||||
position: "absolute",
|
||||
top: "0px",
|
||||
left: "0px",
|
||||
marginLeft: "calc(100% + 10px)",
|
||||
width: "60px",
|
||||
transition: "opacity 500ms",
|
||||
transitionDelay: "1000ms",
|
||||
color: colors.dark ? 'rgba(220,240,230, 1.0)' : 'rgba(0,0,0, 0.8)',
|
||||
},
|
||||
'.code-blocks-math-result-copied.fade-out': {
|
||||
opacity: 0,
|
||||
},
|
||||
// 数学计算结果(自定义)
|
||||
'.code-blocks-math-result': {
|
||||
paddingLeft: "12px",
|
||||
position: "relative",
|
||||
},
|
||||
".code-blocks-math-result .inner": {
|
||||
background: colors.dark ? '#0e1217' : '#48b57e',
|
||||
color: colors.dark ? '#a0e7c7' : '#fff',
|
||||
padding: '0px 4px',
|
||||
borderRadius: '2px',
|
||||
boxShadow: colors.dark ? '0 0 3px rgba(0,0,0, 0.3)' : '0 0 3px rgba(0,0,0, 0.1)',
|
||||
cursor: 'pointer',
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
'.code-blocks-math-result-copied': {
|
||||
position: "absolute",
|
||||
top: "0px",
|
||||
left: "0px",
|
||||
marginLeft: "calc(100% + 10px)",
|
||||
width: "60px",
|
||||
transition: "opacity 500ms",
|
||||
transitionDelay: "1000ms",
|
||||
color: colors.dark ? 'rgba(220,240,230, 1.0)' : 'rgba(0,0,0, 0.8)',
|
||||
},
|
||||
'.code-blocks-math-result-copied.fade-out': {
|
||||
opacity: 0,
|
||||
},
|
||||
|
||||
// 代码块开始标记(自定义)
|
||||
'.code-block-start': {
|
||||
height: '12px',
|
||||
position: 'relative',
|
||||
},
|
||||
'.code-block-start.first': {
|
||||
height: '0px',
|
||||
},
|
||||
}, {dark: colors.dark});
|
||||
// 代码块开始标记(自定义)
|
||||
'.code-block-start': {
|
||||
height: '12px',
|
||||
position: 'relative',
|
||||
},
|
||||
'.code-block-start.first': {
|
||||
height: '0px',
|
||||
},
|
||||
}, {dark: colors.dark});
|
||||
|
||||
// 语法高亮样式
|
||||
const highlightStyle = HighlightStyle.define([
|
||||
// 关键字
|
||||
{tag: tags.keyword, color: colors.keyword},
|
||||
|
||||
// 操作符
|
||||
{tag: [tags.operator, tags.operatorKeyword], color: colors.operator},
|
||||
|
||||
// 名称、变量
|
||||
{tag: [tags.name, tags.deleted, tags.character, tags.macroName], color: colors.variable},
|
||||
{tag: [tags.variableName], color: colors.variable},
|
||||
{tag: [tags.labelName], color: colors.operator},
|
||||
{tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: colors.variable},
|
||||
|
||||
// 函数
|
||||
{tag: [tags.function(tags.variableName)], color: colors.function},
|
||||
{tag: [tags.propertyName], color: colors.function},
|
||||
|
||||
// 类型、类
|
||||
{tag: [tags.typeName], color: colors.type},
|
||||
{tag: [tags.className], color: colors.class},
|
||||
|
||||
// 常量
|
||||
{tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: colors.constant},
|
||||
|
||||
// 字符串
|
||||
{tag: [tags.processingInstruction, tags.string, tags.inserted], color: colors.string},
|
||||
{tag: [tags.special(tags.string)], color: colors.string},
|
||||
{tag: [tags.quote], color: colors.comment},
|
||||
|
||||
// 数字
|
||||
{tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace], color: colors.number},
|
||||
|
||||
// 正则表达式
|
||||
{tag: [tags.url, tags.escape, tags.regexp, tags.link], color: colors.regexp},
|
||||
|
||||
// 注释
|
||||
{tag: [tags.meta, tags.comment], color: colors.comment, fontStyle: 'italic'},
|
||||
|
||||
// 分隔符、括号
|
||||
{tag: [tags.definition(tags.name), tags.separator], color: colors.variable},
|
||||
{tag: [tags.brace], color: colors.variable},
|
||||
{tag: [tags.squareBracket], color: colors.dark ? '#bf616a' : colors.keyword},
|
||||
{tag: [tags.angleBracket], color: colors.dark ? '#d08770' : colors.operator},
|
||||
{tag: [tags.attributeName], color: colors.variable},
|
||||
|
||||
// 标签
|
||||
{tag: [tags.tagName], color: colors.number},
|
||||
|
||||
// 注解
|
||||
{tag: [tags.annotation], color: colors.invalid},
|
||||
|
||||
// 特殊样式
|
||||
{tag: tags.strong, fontWeight: 'bold'},
|
||||
{tag: tags.emphasis, fontStyle: 'italic'},
|
||||
{tag: tags.strikethrough, textDecoration: 'line-through'},
|
||||
{tag: tags.link, color: colors.variable, textDecoration: 'underline'},
|
||||
|
||||
// 标题
|
||||
{tag: tags.heading, fontWeight: 'bold', color: colors.heading},
|
||||
{tag: [tags.heading1, tags.heading2], fontSize: '1.4em'},
|
||||
{tag: [tags.heading3, tags.heading4], fontSize: '1.2em'},
|
||||
{tag: [tags.heading5, tags.heading6], fontSize: '1.1em'},
|
||||
|
||||
// 无效内容
|
||||
{tag: tags.invalid, color: colors.invalid},
|
||||
]);
|
||||
// 语法高亮样式
|
||||
const highlightStyle = HighlightStyle.define([
|
||||
// 关键字
|
||||
{tag: tags.keyword, color: colors.keyword},
|
||||
|
||||
return [
|
||||
theme,
|
||||
syntaxHighlighting(highlightStyle),
|
||||
];
|
||||
// 操作符
|
||||
{tag: [tags.operator, tags.operatorKeyword], color: colors.operator},
|
||||
|
||||
// 名称、变量
|
||||
{tag: [tags.name, tags.deleted, tags.character, tags.macroName], color: colors.variable},
|
||||
{tag: [tags.variableName], color: colors.variable},
|
||||
{tag: [tags.labelName], color: colors.operator},
|
||||
{tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: colors.variable},
|
||||
|
||||
// 函数
|
||||
{tag: [tags.function(tags.variableName)], color: colors.function},
|
||||
{tag: [tags.propertyName], color: colors.function},
|
||||
|
||||
// 类型、类
|
||||
{tag: [tags.typeName], color: colors.type},
|
||||
{tag: [tags.className], color: colors.class},
|
||||
|
||||
// 常量
|
||||
{tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: colors.constant},
|
||||
|
||||
// 字符串
|
||||
{tag: [tags.processingInstruction, tags.string, tags.inserted], color: colors.string},
|
||||
{tag: [tags.special(tags.string)], color: colors.string},
|
||||
{tag: [tags.quote], color: colors.comment},
|
||||
|
||||
// 数字
|
||||
{
|
||||
tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace],
|
||||
color: colors.number
|
||||
},
|
||||
|
||||
// 正则表达式
|
||||
{tag: [tags.url, tags.escape, tags.regexp, tags.link], color: colors.regexp},
|
||||
|
||||
// 注释
|
||||
{tag: [tags.meta, tags.comment], color: colors.comment, fontStyle: 'italic'},
|
||||
|
||||
// 分隔符、括号
|
||||
{tag: [tags.definition(tags.name), tags.separator], color: colors.variable},
|
||||
{tag: [tags.brace], color: colors.variable},
|
||||
{tag: [tags.squareBracket], color: colors.dark ? '#bf616a' : colors.keyword},
|
||||
{tag: [tags.angleBracket], color: colors.dark ? '#d08770' : colors.operator},
|
||||
{tag: [tags.attributeName], color: colors.variable},
|
||||
|
||||
// 标签
|
||||
{tag: [tags.tagName], color: colors.number},
|
||||
|
||||
// 注解
|
||||
{tag: [tags.annotation], color: colors.invalid},
|
||||
|
||||
// 特殊样式
|
||||
{tag: tags.strong, fontWeight: 'bold'},
|
||||
{tag: tags.emphasis, fontStyle: 'italic'},
|
||||
{tag: tags.strikethrough, textDecoration: 'line-through'},
|
||||
{tag: tags.link, color: colors.variable, textDecoration: 'underline'},
|
||||
|
||||
// 标题
|
||||
{tag: tags.heading, fontWeight: 'bold', color: colors.heading},
|
||||
{tag: [tags.heading1, tags.heading2], fontSize: '1.4em'},
|
||||
{tag: [tags.heading3, tags.heading4], fontSize: '1.2em'},
|
||||
{tag: [tags.heading5, tags.heading6], fontSize: '1.1em'},
|
||||
|
||||
// 无效内容
|
||||
{tag: tags.invalid, color: colors.invalid},
|
||||
]);
|
||||
|
||||
return [
|
||||
theme,
|
||||
syntaxHighlighting(highlightStyle),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user