chore: standardize project comments in English
This commit is contained in:
1 parent
a874a39ea5
commit
ccb16acbea
167 files changed
+820
-820
No files matched your search
@@ -10,10 +10,10 @@ import { editorFocused, setEditorFocus } from './focus'
|
||||
import { editorLocale } from './locale'
|
||||
import { CodeRendererWidget, MathWidget } from './widgets'
|
||||
|
||||
// 公式块与 Mermaid 图表:未编辑时整块替换为渲染结果;光标在块内时显示源码,并在下方实时预览。
|
||||
// 跨行的块级替换只能由 StateField 提供;与表格一样只处理顶层的块
|
||||
// Math and Mermaid blocks render as replacements while inactive; show source and a live preview below when the cursor enters the block.
|
||||
// Multiline block replacements must come from StateField; handle only top-level blocks, as with tables.
|
||||
|
||||
/** 返回块的源码内容及其相对块起点的偏移(点击渲染结果时光标落在内容开头) */
|
||||
/** Return block source and its offset from the block start so clicking rendered output places the cursor at the content start. */
|
||||
function blockContent(state: EditorState, node: SyntaxNode): { text: string, offset: number, language?: string } | null {
|
||||
if (node.name === 'BlockMath') {
|
||||
const marks = node.getChildren('BlockMathMark')
|
||||
|
||||
@@ -8,7 +8,7 @@ import { StateEffect, StateField } from '@codemirror/state'
|
||||
import { Decoration, EditorView, ViewPlugin } from '@codemirror/view'
|
||||
|
||||
interface CodeBlock {
|
||||
// 代码内容(不含围栏行)在文档中的范围
|
||||
// Document range of code contents, excluding fence lines.
|
||||
from: number
|
||||
to: number
|
||||
lang: string
|
||||
@@ -17,7 +17,7 @@ interface CodeBlock {
|
||||
|
||||
const setBlockTokens = StateEffect.define<{ from: number, to: number, decorations: Range<Decoration>[] }>()
|
||||
|
||||
// 高亮结果作为 StateField 保存并随编辑映射位置:Worker 返回新结果前沿用旧颜色,避免闪烁
|
||||
// Keep highlighting in a StateField and map positions through edits; retain old colors until the Worker responds to avoid flashing.
|
||||
const codeTokenField = StateField.define<DecorationSet>({
|
||||
create: () => Decoration.none,
|
||||
update(value, tr) {
|
||||
@@ -72,7 +72,7 @@ export function fenceLanguage(state: EditorState, infoFrom: number, infoTo: numb
|
||||
return state.sliceDoc(infoFrom, infoTo).trim().split(/\s/)[0].toLowerCase()
|
||||
}
|
||||
|
||||
// 围栏代码块的内容范围(不含围栏行);有闭合围栏时到闭合行之前为止,否则到块尾
|
||||
// Fenced-code content range, excluding fence lines; stop before the closing line if present, otherwise at the block end.
|
||||
export function codeContentRange(state: EditorState, node: SyntaxNode): { from: number, to: number } {
|
||||
const open = state.doc.lineAt(node.from)
|
||||
const marks = node.getChildren('CodeMark')
|
||||
@@ -87,7 +87,7 @@ export function fencedCodeAt(state: EditorState, pos: number): SyntaxNode | null
|
||||
return null
|
||||
}
|
||||
|
||||
// Ctrl/Cmd+A:光标在代码块内时先选中整块代码,已选中整块时交给默认的全选
|
||||
// Ctrl/Cmd+A first selects the current code block's contents; when already selected, defer to normal select-all.
|
||||
export function selectCodeBlock(view: EditorView): boolean {
|
||||
const { state } = view
|
||||
const main = state.selection.main
|
||||
@@ -124,7 +124,7 @@ function visibleCodeBlocks(view: EditorView): CodeBlock[] {
|
||||
|
||||
const codeHighlighter = ViewPlugin.fromClass(
|
||||
class {
|
||||
// 已应用到各代码块的结果键,按块起点记录
|
||||
// Result keys applied to code blocks, indexed by block start.
|
||||
applied = new Map<number, string>()
|
||||
destroyed = false
|
||||
|
||||
@@ -141,7 +141,7 @@ const codeHighlighter = ViewPlugin.fromClass(
|
||||
if (u.docChanged || u.viewportChanged || syntaxTree(u.startState) !== syntaxTree(u.state)) this.schedule()
|
||||
}
|
||||
|
||||
// 不能在 update 中同步 dispatch,推迟到微任务
|
||||
// Do not dispatch synchronously inside update; defer to a microtask.
|
||||
schedule() {
|
||||
queueMicrotask(() => this.sync())
|
||||
}
|
||||
@@ -161,9 +161,9 @@ const codeHighlighter = ViewPlugin.fromClass(
|
||||
this.applied.set(block.from, key)
|
||||
effects.push(setBlockTokens.of({ from: block.from, to: block.to, decorations: tokensToDecorations(block, cached) }))
|
||||
} else {
|
||||
// 结果到达后再走一次 sync,届时命中缓存;期间文档若已变化,键不同则自然重算
|
||||
// Run sync again when results arrive to hit the cache; changed document keys naturally trigger recalculation.
|
||||
requestTokens(block.lang, block.code).then(() => this.schedule()).catch((error) => {
|
||||
// 本次内容失败后不随每次光标移动重试,修改代码或语言后重新请求。
|
||||
// Do not retry failed content on every cursor movement; request again after code or language changes.
|
||||
if (!this.destroyed) this.applied.set(block.from, key)
|
||||
console.error('代码高亮失败', error)
|
||||
})
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { EditorView, ViewUpdate } from '@codemirror/view'
|
||||
import { StateEffect, StateField } from '@codemirror/state'
|
||||
import { ViewPlugin } from '@codemirror/view'
|
||||
|
||||
// 编辑器焦点状态。块级渲染组件(表格、公式块、Mermaid)由 StateField 提供,拿不到视图的焦点,
|
||||
// 由视图插件把焦点变化同步进状态:获得焦点且光标在块内时显示源码,否则渲染
|
||||
// (打开笔记时光标默认在文首,不应因此展开开头的块)
|
||||
// Editor focus state. StateField-based block widgets (tables, math, Mermaid) cannot access view focus directly,
|
||||
// so a view plugin syncs focus into state: show source only when focused with the cursor inside the block; otherwise render.
|
||||
// Opening a note places the cursor at the start and must not expand its first block by itself.
|
||||
export const setEditorFocus = StateEffect.define<boolean>()
|
||||
|
||||
export const editorFocused = StateField.define<boolean>({
|
||||
@@ -27,7 +27,7 @@ const focusTracker = ViewPlugin.fromClass(
|
||||
if (u.focusChanged) this.sync()
|
||||
}
|
||||
|
||||
// 不能在 update 中同步 dispatch,推迟到微任务
|
||||
// Do not dispatch synchronously inside update; defer to a microtask.
|
||||
sync() {
|
||||
queueMicrotask(() => {
|
||||
const focused = this.view.hasFocus
|
||||
|
||||
@@ -3,7 +3,7 @@ import { EditorSelection, EditorState } from '@codemirror/state'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { cycleHeading, insertLink, toggleBlock, toggleBold, toggleCode, toggleItalic } from './format'
|
||||
|
||||
// 用 | 标记光标、[ ] 标记选区,执行命令后以同样的形式返回结果
|
||||
// Mark the cursor with | and selection with [ ]; return the result in the same notation after executing the command.
|
||||
function run(cmd: FormatCommand, doc: string): string {
|
||||
const ranges: { anchor: number, head: number }[] = []
|
||||
let text = ''
|
||||
@@ -59,7 +59,7 @@ describe('行首标记', () => {
|
||||
it('多行:有序列表按行编号,保留缩进', () => {
|
||||
expect(run(toggleBlock('ordered'), '[a\n b\nc]')).toBe('[1. a\n 2. b\n3. c]')
|
||||
expect(run(toggleBlock('ordered'), '[1. a\n2. b]')).toBe('[a\nb]')
|
||||
// 部分行已是该类型时统一加上
|
||||
// Apply the type consistently when only some lines already have it.
|
||||
expect(run(toggleBlock('bullet'), '[- a\nb]')).toBe('[- a\n- b]')
|
||||
})
|
||||
it('标题循环', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ChangeSpec, EditorState, TransactionSpec } from '@codemirror/state'
|
||||
// Markdown 格式命令:快捷键与选区工具栏共用
|
||||
// Markdown formatting commands shared by shortcuts and the selection toolbar.
|
||||
import { EditorSelection } from '@codemirror/state'
|
||||
|
||||
/** 只依赖状态与派发,便于在没有 DOM 的环境中测试 */
|
||||
/** Depend only on state and dispatch to allow testing without a DOM. */
|
||||
export interface FormatTarget {
|
||||
readonly state: EditorState
|
||||
dispatch: (spec: TransactionSpec) => void
|
||||
@@ -10,13 +10,13 @@ export interface FormatTarget {
|
||||
export type FormatCommand = (target: FormatTarget) => boolean
|
||||
|
||||
/**
|
||||
* 切换行内标记(粗体 **、斜体 *、删除线 ~~、行内代码 `):选区两侧或选区首尾已有该标记时去掉,否则加上;
|
||||
* 没有选中文字时插入一对标记,光标放在中间
|
||||
* Toggle inline markers (bold **, italic *, strikethrough ~~, inline code `): remove matching surrounding or edge
|
||||
* markers, otherwise add them. With no selection, insert a pair and place the cursor between them.
|
||||
*/
|
||||
export function toggleInline(mark: string): FormatCommand {
|
||||
const n = mark.length
|
||||
const ch = mark[0]
|
||||
// 选区两侧连续的标记字符数;* 同时用于斜体与粗体:*** 两者都有,单数个才有斜体
|
||||
// Count adjacent marker characters around the selection; * serves italic and bold: *** means both, and only odd counts imply italic.
|
||||
const runLength = (state: EditorState, pos: number, dir: 1 | -1) => {
|
||||
let k = 0
|
||||
while (state.sliceDoc(dir > 0 ? pos + k : pos - k - 1, dir > 0 ? pos + k + 1 : pos - k) === ch) k++
|
||||
@@ -54,7 +54,7 @@ export function toggleInline(mark: string): FormatCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// 行首的块标记:缩进 + 标题 / 任务 / 无序列表 / 有序列表 / 引用(每行只识别一个)
|
||||
// Leading block marker: indentation plus heading/task/bullet/ordered-list/quote (recognize one per line).
|
||||
const LINE_PREFIX = /^(\s*)(#{1,6} |[-*+] \[[ x]\] |[-*+] |\d+[.)] |> )?/i
|
||||
|
||||
type PrefixKind = 'heading' | 'task' | 'bullet' | 'ordered' | 'quote'
|
||||
@@ -67,7 +67,7 @@ function kindOf(prefix: string): PrefixKind | null {
|
||||
return prefix.includes('[') ? 'task' : 'bullet'
|
||||
}
|
||||
|
||||
/** 改写选区涉及的每一行的行首标记;next 返回新的标记 */
|
||||
/** Rewrite leading markers on every selected line; next returns the new marker. */
|
||||
function editPrefixes({ state, dispatch }: FormatTarget, next: (kinds: (PrefixKind | null)[], index: number) => string): boolean {
|
||||
const lineNumbers = new Set<number>()
|
||||
for (const r of state.selection.ranges) {
|
||||
@@ -83,7 +83,7 @@ function editPrefixes({ state, dispatch }: FormatTarget, next: (kinds: (PrefixKi
|
||||
return { from, to: from + current.length, insert: next(kinds, i) }
|
||||
})
|
||||
const changes = state.changes(specs)
|
||||
// 光标在行首时随新插入的标记后移;选区则向两端扩展,把新标记包含在内
|
||||
// Move a line-start cursor after the inserted marker; expand selections outward to include new markers.
|
||||
const selection = EditorSelection.create(
|
||||
state.selection.ranges.map((r) => {
|
||||
if (r.empty) return EditorSelection.cursor(changes.mapPos(r.head, 1))
|
||||
@@ -96,7 +96,7 @@ function editPrefixes({ state, dispatch }: FormatTarget, next: (kinds: (PrefixKi
|
||||
return true
|
||||
}
|
||||
|
||||
/** 切换列表或引用:所选各行都已是该类型时去掉,否则全部改为该类型(替换原有的其他块标记) */
|
||||
/** Toggle a list or quote: remove the type if all selected lines have it; otherwise apply it to all, replacing other block markers. */
|
||||
export function toggleBlock(kind: Exclude<PrefixKind, 'heading'>): FormatCommand {
|
||||
return target =>
|
||||
editPrefixes(target, (kinds, i) => {
|
||||
@@ -105,7 +105,7 @@ export function toggleBlock(kind: Exclude<PrefixKind, 'heading'>): FormatCommand
|
||||
})
|
||||
}
|
||||
|
||||
/** 标题级别循环:正文 → 一级 → 二级 → 三级 → 正文;以第一行为准 */
|
||||
/** Cycle heading levels based on the first line: body -> level 1 -> level 2 -> level 3 -> body. */
|
||||
export const cycleHeading: FormatCommand = (target) => {
|
||||
const first = LINE_PREFIX.exec(target.state.doc.lineAt(target.state.selection.main.from).text)![2] ?? ''
|
||||
const level = kindOf(first) === 'heading' ? first.length - 1 : 0
|
||||
@@ -113,7 +113,7 @@ export const cycleHeading: FormatCommand = (target) => {
|
||||
return editPrefixes(target, () => prefix)
|
||||
}
|
||||
|
||||
/** 全部选中行已是指定标题时恢复正文,否则设置为指定级别。 */
|
||||
/** Return to body text if every selected line already has the requested heading level; otherwise apply that level. */
|
||||
export function toggleHeading(level: number): FormatCommand {
|
||||
return (target) => {
|
||||
const prefix = `${'#'.repeat(level)} `
|
||||
@@ -128,7 +128,7 @@ export function toggleHeading(level: number): FormatCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/** 插入链接:选中文字作为链接文字,光标放到地址处;没有选中时光标放在文字处 */
|
||||
/** Insert a link using selected text as its label and focus the destination; without a selection, focus the label. */
|
||||
export const insertLink: FormatCommand = ({ state, dispatch }) => {
|
||||
const tr = state.changeByRange((r) => {
|
||||
const text = state.sliceDoc(r.from, r.to)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { t as translate } from '@@/i18n/runtime'
|
||||
/* eslint-disable no-control-regex -- 粘贴链接必须识别并移除协议中的控制字符。 */
|
||||
/* eslint-disable no-control-regex -- Pasted links must detect and remove control characters in schemes. */
|
||||
import TurndownService from 'turndown'
|
||||
|
||||
export async function htmlToMarkdown(html: string, importImage: (url: string) => Promise<string>, onError: (error: unknown) => void): Promise<string> {
|
||||
// template 中的外部资源和脚本保持惰性,不让剪贴板 HTML 进入活动页面。
|
||||
// Keep external resources and scripts inert inside template; clipboard HTML must not enter the active page.
|
||||
const template = document.createElement('template')
|
||||
template.innerHTML = html
|
||||
const fragment = template.content
|
||||
@@ -32,7 +32,7 @@ export async function htmlToMarkdown(html: string, importImage: (url: string) =>
|
||||
images.set(src, group)
|
||||
}
|
||||
const sources = [...images]
|
||||
// 网页常有多张图片;限制并行数量,避免一次粘贴占满浏览器连接和服务端下载内存。
|
||||
// Web pages often contain many images; bound concurrency so one paste cannot exhaust browser connections or server download memory.
|
||||
for (let offset = 0; offset < sources.length; offset += 3) {
|
||||
await Promise.all(sources.slice(offset, offset + 3).map(async ([src, nodes]) => {
|
||||
try {
|
||||
@@ -61,7 +61,7 @@ export async function htmlToMarkdown(html: string, importImage: (url: string) =>
|
||||
return `\n\n${[line(rows[0]), line(Array.from<string>({ length: columns }).fill('---')), ...rows.slice(1).map(line)].join('\n')}\n\n`
|
||||
},
|
||||
})
|
||||
// 链接目标用尖括号包裹,避免 URL 中的括号截断 Markdown;不保留可执行协议。
|
||||
// Wrap destinations in angle brackets so URL parentheses do not truncate Markdown; discard executable schemes.
|
||||
service.addRule('link', {
|
||||
filter: 'a',
|
||||
replacement: (content, node) => {
|
||||
|
||||
@@ -20,8 +20,8 @@ import { markdownTheme } from './theme'
|
||||
|
||||
export { insertImageFiles, pasteImages } from './paste'
|
||||
|
||||
// 在 lang-markdown 的语法集(CommonMark + GFM + 上下标 + emoji)上加 Typora 扩展。
|
||||
// 沿用其语言数据,markdownKeymap 中的命令据此识别 Markdown(isActiveAt 按语言数据判断)
|
||||
// Add Typora extensions to lang-markdown's syntax set (CommonMark, GFM, superscript/subscript, and emoji).
|
||||
// Reuse its language data so markdownKeymap commands recognize Markdown through isActiveAt.
|
||||
export const pluginCompartment = new Compartment()
|
||||
export const languageCompartment = new Compartment()
|
||||
export const preferencesCompartment = new Compartment()
|
||||
@@ -49,20 +49,20 @@ export interface EditorStateOptions {
|
||||
shortcuts?: KeyBinding[]
|
||||
resolveImage?: (src: string) => string
|
||||
openLink?: (href: string) => void
|
||||
// 文档变化回调,仅对挂载在视图上的状态触发;未激活标签页的状态由调用方直接更新
|
||||
// Notify document changes only for the state mounted in the view; callers update inactive-tab states directly.
|
||||
onChange?: (update: ViewUpdate) => void
|
||||
extensions?: Extension[]
|
||||
}
|
||||
|
||||
// 每个打开的笔记各自持有一个 EditorState(含撤销历史),切换标签页时整体替换
|
||||
// Each open note owns an EditorState with undo history; replace the entire state on tab switches.
|
||||
export function createEditorState(opts: EditorStateOptions): EditorState {
|
||||
const extensions: Extension[] = [
|
||||
history(),
|
||||
localeCompartment.of(editorLocale.of(locale.value)),
|
||||
drawSelection(),
|
||||
EditorView.lineWrapping,
|
||||
// 不用 lang-markdown 的 markdown():它会静态引入 lang-html/css/javascript 用于嵌套 HTML 高亮,
|
||||
// 首屏包 gzip 从约 108 KB 增至约 180 KB,而 Live Preview 不需要 HTML 高亮
|
||||
// Avoid lang-markdown's markdown(): its static lang-html/css/javascript imports enable nested HTML highlighting,
|
||||
// increasing the initial gzip bundle from roughly 108 KB to 180 KB, while Live Preview does not need HTML highlighting.
|
||||
languageCompartment.of(pluginLanguage()),
|
||||
pluginCompartment.of(pluginEditorExtensions()),
|
||||
EditorView.exceptionSink.of(reportEditorFailure),
|
||||
@@ -72,7 +72,7 @@ export function createEditorState(opts: EditorStateOptions): EditorState {
|
||||
codeBlockHighlight,
|
||||
editorFocus,
|
||||
previewCompartment.of(previewExtensions(opts.sourceMode ?? false)),
|
||||
// 可配置操作不再通过 CM 的默认绑定重复注册。
|
||||
// Do not register configurable actions again through CodeMirror's default bindings.
|
||||
keymap.of(defaultKeymap.filter(binding => binding.run !== selectAll && binding.run !== indentLess && binding.run !== indentMore && binding.run !== selectParentSyntax && binding.run !== toggleComment)),
|
||||
preferencesCompartment.of([
|
||||
EditorState.tabSize.of(opts.tabSize ?? 4),
|
||||
|
||||
@@ -24,16 +24,16 @@ const MARK_NODES: Record<InlineMark, string> = { StrongEmphasis: 'EmphasisMark',
|
||||
|
||||
function formattingTree(state: EditorState) {
|
||||
const to = state.selection.ranges.at(-1)!.to
|
||||
// 后台标签页或长文末尾的选区可能尚未完成增量解析;操作不能悄悄漏掉文字。
|
||||
// Selections in background tabs or near long-document ends may not be fully parsed; operations must not silently skip text.
|
||||
return ensureSyntaxTree(state, to, 50) ?? state.facet(language)!.parser.parse(state.doc.toString())
|
||||
}
|
||||
|
||||
/** 从当前语法树取文字片段,保留 Markdown 的标记、链接地址和块级结构。 */
|
||||
/** Extract text fragments from the current syntax tree, preserving Markdown markers, link destinations, and block structure. */
|
||||
function segments(state: EditorState): Segment[] {
|
||||
const result: Segment[] = []
|
||||
function walk(node: SyntaxNode, styles: InlineStyles = {}, underline = false, formats: string[] = [], wrappers: Wrapper[] = []) {
|
||||
if (['InlineCode', 'InlineMath', 'Image', 'FootnoteReference', 'Emoji', 'Autolink', 'Escape', 'Entity'].includes(node.name)) {
|
||||
// 不拆开原子语法;调整同一外层标签的其他文字时,仍须保留这些未选内容的原样式。
|
||||
// Do not split atomic syntax; when changing sibling text under the same outer tag, preserve styles on these unselected fragments.
|
||||
result.push({ from: node.from, to: node.to, atomic: true, styles, underline, formats, wrappers })
|
||||
return
|
||||
}
|
||||
@@ -54,7 +54,7 @@ function segments(state: EditorState): Segment[] {
|
||||
for (const match of state.sliceDoc(from, to).matchAll(/[^\n]+/g)) {
|
||||
const start = from + match.index; const end = start + match[0].length
|
||||
const line = state.doc.lineAt(start)
|
||||
// 连续段落里的下一行仍可能带列表缩进或引用前缀,标签必须写在前缀之后。
|
||||
// Continuation lines can still have list indentation or quote prefixes; insert tags after the prefix.
|
||||
const prefixEnd = line.from + /^(?:[\t ]*>[\t ]?)*[\t ]*/.exec(line.text)![0].length
|
||||
const contentFrom = Math.max(start, prefixEnd)
|
||||
if (contentFrom < end) result.push({ from: contentFrom, to: end, atomic: false, styles, underline, formats, wrappers })
|
||||
@@ -116,7 +116,7 @@ function wrap(text: string, styles: InlineStyles, underline: boolean, formats: s
|
||||
return { prefix, leading: leading.length, trailing: trailing.length, marks: open.length, text: prefix + (content ? leading + open + content + close + trailing : text) + (underline ? '</u>' : '') + (css ? '</span>' : '') }
|
||||
}
|
||||
|
||||
/** 只重写选中文字及包围它的外观标签;被拆开的其余文字保留原样式。 */
|
||||
/** Rewrite only selected text and surrounding appearance tags; retain styles on remaining split fragments. */
|
||||
function editAppearance(update: (styles: InlineStyles, underline: boolean, formats: string[]) => { styles: InlineStyles, underline: boolean, formats: string[] }): FormatCommand {
|
||||
return ({ state, dispatch }) => {
|
||||
if (!canFormatSelection(state)) return false
|
||||
@@ -130,14 +130,14 @@ function editAppearance(update: (styles: InlineStyles, underline: boolean, forma
|
||||
}
|
||||
for (const run of runs) {
|
||||
if (!selected(state, run) && !run.wrappers.some(wrapper => wrappers.has(wrapper))) continue
|
||||
// 同一外层标签内的兄弟标签也要展开,重新写回合并后的有效样式。
|
||||
// Expand sibling tags within the same outer tag too, then write back merged effective styles.
|
||||
for (const wrapper of run.wrappers) {
|
||||
if (!wrappers.has(wrapper)) {
|
||||
wrappers.add(wrapper)
|
||||
for (const mark of wrapper.marks) changes.push({ from: mark.from, to: mark.to, insert: '' })
|
||||
}
|
||||
}
|
||||
// 部分选中转义字符等语法单元时,格式应用于整个单元,不能把标签插进语法内部。
|
||||
// Partial selections of atomic syntax, such as escapes, format the whole unit; never insert tags inside the syntax.
|
||||
const cuts = run.atomic ? [run.from, run.to] : [...new Set([run.from, run.to, ...state.selection.ranges.flatMap(range => [Math.max(run.from, Math.min(run.to, range.from)), Math.max(run.from, Math.min(run.to, range.to))])])].sort((a, b) => a - b)
|
||||
let insert = ''
|
||||
for (let i = 0; i < cuts.length - 1; i++) {
|
||||
@@ -184,7 +184,7 @@ export const toggleUnderline: FormatCommand = (target) => {
|
||||
return editAppearance((styles, _underline, formats) => ({ styles, underline: !remove, formats }))(target)
|
||||
}
|
||||
|
||||
/** 跨段落格式化按语法片段处理,不把列表标记或链接地址包进 Markdown 定界符。 */
|
||||
/** Format across paragraphs by syntax fragment, without wrapping list markers or link destinations in Markdown delimiters. */
|
||||
export function toggleSelectionMark(mark: InlineMark): FormatCommand {
|
||||
return (target) => {
|
||||
const runs = segments(target.state).filter(run => selected(target.state, run))
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('快捷键', () => {
|
||||
it('windows 上 Ctrl+Y 为重做', async () => {
|
||||
// CodeMirror 在模块加载时读取平台,需先设置再导入
|
||||
// CodeMirror reads the platform at module load; configure it before importing.
|
||||
Object.defineProperty(navigator, 'platform', { value: 'Win32', configurable: true })
|
||||
const { undo } = await import('@codemirror/commands')
|
||||
const { EditorView, runScopeHandlers } = await import('@codemirror/view')
|
||||
|
||||
@@ -21,14 +21,14 @@ import {
|
||||
TocWidget,
|
||||
} from './widgets'
|
||||
|
||||
// 把笔记中书写的图片地址解析为浏览器可加载的 URL(相对路径需结合笔记所在目录)
|
||||
// Resolve note image paths to browser-loadable URLs, using the note's folder for relative paths.
|
||||
export const resolveImageUrl = Facet.define<(src: string) => string, (src: string) => string>({
|
||||
combine: values => values[0] ?? (src => src),
|
||||
})
|
||||
|
||||
export const openLink = Facet.define<(href: string) => void, ((href: string) => void) | undefined>({ combine: values => values[0] })
|
||||
|
||||
// 图片文件原位替换时重建预览,不改变正文、选区或撤销历史。
|
||||
// Rebuild previews after in-place image replacement without changing text, selection, or undo history.
|
||||
export const refreshImages = StateEffect.define<null>()
|
||||
|
||||
const hidden = Decoration.replace({})
|
||||
@@ -46,8 +46,8 @@ function lineDeco(cls: string): Decoration {
|
||||
return deco
|
||||
}
|
||||
|
||||
// 块级标记(标题、引用、列表、围栏)按行展开源码,行内语法只展开光标所在的元素,
|
||||
// 与 Typora/Obsidian 一致,避免长段落中光标一落整行跳动;编辑器失焦时整篇按渲染效果显示
|
||||
// Expand block markers (headings, quotes, lists, fences) by line; expand inline syntax only around the cursor,
|
||||
// matching Typora/Obsidian and avoiding whole-line jumps in long paragraphs. Render the entire document when the editor loses focus.
|
||||
function activeLines(view: EditorView): Set<number> {
|
||||
const lines = new Set<number>()
|
||||
if (!view.hasFocus) return lines
|
||||
@@ -74,7 +74,7 @@ function buildDecorations(view: EditorView): DecorationSet {
|
||||
}
|
||||
const ranges = view.hasFocus ? state.selection.ranges : []
|
||||
const touches = (node: SyntaxNode) => ranges.some(r => r.from <= node.to && r.to >= node.from)
|
||||
// 视图插件提供的替换装饰不能跨越换行
|
||||
// Replacement decorations from view plugins cannot cross newlines.
|
||||
const hide = (from: number, to: number) => {
|
||||
if (from < to && lineOf(from) === lineOf(to)) out.push(hidden.range(from, to))
|
||||
}
|
||||
@@ -93,7 +93,7 @@ function buildDecorations(view: EditorView): DecorationSet {
|
||||
return end
|
||||
}
|
||||
|
||||
// 脚注定义(标签 → 内容)与目录条目,只在可见区域出现相应语法时才计算
|
||||
// Compute footnote definitions (label -> content) and table-of-contents entries only when the relevant syntax is visible.
|
||||
let footnoteMap: Map<string, string> | undefined
|
||||
function footnotes() {
|
||||
if (!footnoteMap) {
|
||||
@@ -195,7 +195,7 @@ function buildDecorations(view: EditorView): DecorationSet {
|
||||
n === first ? 'cm-rm-frontmatter cm-rm-frontmatter-begin' : n === last ? 'cm-rm-frontmatter cm-rm-frontmatter-end' : 'cm-rm-frontmatter')
|
||||
return false
|
||||
case 'BlockMath':
|
||||
// 未编辑时由 blocks.ts 整块替换为渲染结果,这里只负责编辑时的源码样式
|
||||
// blocks.ts replaces inactive blocks with rendered output; this code handles source styling only while editing.
|
||||
classLines(node.from, node.to, () => 'cm-rm-math-source')
|
||||
return false
|
||||
case 'InlineCode':
|
||||
@@ -244,7 +244,7 @@ function buildDecorations(view: EditorView): DecorationSet {
|
||||
classLines(node.from, node.to, () => 'cm-rm-code')
|
||||
return false
|
||||
case 'Table':
|
||||
// 光标在表格内时由此以等宽源码显示(其余情况由 table.ts 渲染为可编辑表格),内部语法不做隐藏以免列对不齐
|
||||
// Show monospaced source while the cursor is in a table (otherwise table.ts renders an editable table); preserve internal syntax to keep columns aligned.
|
||||
classLines(node.from, node.to, () => 'cm-rm-table')
|
||||
return false
|
||||
case 'HTMLBlock':
|
||||
@@ -290,7 +290,7 @@ function buildDecorations(view: EditorView): DecorationSet {
|
||||
const openLine = doc.lineAt(node.from)
|
||||
const lang = info ? fenceLanguage(state, info.from, info.to) : ''
|
||||
if (isActive(node.from, node.to)) {
|
||||
// 编辑中显示围栏源码,复制按钮浮在行尾
|
||||
// Show fence source while editing, with the copy button floating at line end.
|
||||
out.push(Decoration.widget({ widget: new CodeHeaderWidget(lang, false, codeAt), side: 1 }).range(openLine.to))
|
||||
return
|
||||
}
|
||||
@@ -319,7 +319,7 @@ function trailingSpaceStart(state: EditorState, pos: number): number {
|
||||
return start
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + 点击打开链接
|
||||
// Ctrl/Cmd-click opens links.
|
||||
function linkAt(state: EditorState, pos: number): string | null {
|
||||
for (let node: SyntaxNode | null = syntaxTree(state).resolveInner(pos, 1); node; node = node.parent) {
|
||||
if (node.name === 'URL') return state.sliceDoc(node.from, node.to).replace(/^<|>$/g, '')
|
||||
@@ -348,7 +348,7 @@ function linkAt(state: EditorState, pos: number): string | null {
|
||||
const livePreviewPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet
|
||||
// 输入法组字期间只映射位置不重建,组字结束后再补一次重建
|
||||
// During IME composition, map positions without rebuilding; rebuild once composition ends.
|
||||
stale = false
|
||||
|
||||
constructor(view: EditorView) {
|
||||
|
||||
@@ -5,15 +5,15 @@ import { Facet, StateEffect, StateField } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
|
||||
export interface PasteImagesOptions {
|
||||
// 文件移动期间不能开始异步粘贴或上传,避免响应中的资源链接属于旧路径。
|
||||
// Do not start asynchronous paste or uploads during file moves; returned resource links could target the old path.
|
||||
canPaste: () => boolean
|
||||
// 上传文件,返回写入 Markdown 的链接
|
||||
// Upload a file and return its Markdown link.
|
||||
upload: (file: File) => Promise<string>
|
||||
importImage: (url: string) => Promise<string>
|
||||
// 按笔记身份更新,异步粘贴或上传结束时该笔记可能已切到后台。
|
||||
// Update by note identity; the note may be in the background when asynchronous paste or upload completes.
|
||||
update: (change: (state: EditorState) => TransactionSpec | undefined) => void
|
||||
onError: (err: unknown) => void
|
||||
// 上传开始/结束,供保存队列在上传期间暂缓保存(避免把临时的 blob: 地址写入文件)
|
||||
// Notify upload start/end so the save queue can pause and avoid writing temporary blob: URLs to files.
|
||||
onPending: (delta: 1 | -1) => void
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ const pendingPastes = StateField.define<Map<object, { from: number, to: number }
|
||||
transaction.changes.iterChangedRanges((from, to) => {
|
||||
if ((from < range.to && to > range.from) || (from === to && from > range.from && from < range.to)) edited = true
|
||||
})
|
||||
// 用户在等待期间编辑或撤销了粘贴内容,迟到的转换结果不能覆盖输入。
|
||||
// 同一位置连续粘贴时按发起顺序定位,与图片下载完成的先后无关。
|
||||
// Edits or undo while waiting must not be overwritten by a late conversion result.
|
||||
// Locate successive pastes at the same position in initiation order, independent of image-download completion order.
|
||||
const association = range.from === range.to ? (afterCompleted ? 1 : -1) : 1
|
||||
if (!edited) next.set(id, { from: transaction.changes.mapPos(range.from, association), to: transaction.changes.mapPos(range.to, range.from === range.to ? association : -1) })
|
||||
}
|
||||
@@ -85,8 +85,8 @@ function insertHTML(view: EditorView, html: string, text: string, opts: PasteIma
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入图片文件(粘贴、拖入、从相册选择共用):先用 blob: 地址插入占位,立即显示;上传完成后替换为正式链接。
|
||||
* 返回是否已处理图片插入(包括因操作冲突被拒绝);未配置上传时不处理
|
||||
* Insert image files from paste, drop, or photo selection: immediately show blob: placeholders, then replace them with
|
||||
* uploaded links. Return whether insertion was handled, including operation-conflict rejection; do nothing if upload is unconfigured.
|
||||
*/
|
||||
export function insertImageFiles(view: EditorView, files: File[], pos?: number): boolean {
|
||||
const opts = view.state.facet(uploadOptions)
|
||||
@@ -120,7 +120,7 @@ export function insertImageFiles(view: EditorView, files: File[], pos?: number):
|
||||
return true
|
||||
}
|
||||
|
||||
// 粘贴或拖入图片
|
||||
// Paste or drop images.
|
||||
export function pasteImages(opts: PasteImagesOptions): Extension {
|
||||
let plain = false
|
||||
return [
|
||||
@@ -144,7 +144,7 @@ export function pasteImages(opts: PasteImagesOptions): Extension {
|
||||
e.preventDefault()
|
||||
return true
|
||||
}
|
||||
// 个别浏览器/来源只在 items 中提供图片
|
||||
// Some browsers/sources expose images only through items.
|
||||
let files = [...(e.clipboardData?.files ?? [])]
|
||||
if (!files.length) {
|
||||
files = [...(e.clipboardData?.items ?? [])].filter(i => i.kind === 'file').map(i => i.getAsFile()).filter(f => f !== null)
|
||||
|
||||
@@ -23,13 +23,13 @@ interface TableModel {
|
||||
from: number
|
||||
to: number
|
||||
aligns: Align[]
|
||||
// rows[0] 为表头,其余为表体;分隔行单独由 aligns 表示
|
||||
// rows[0] is the header; remaining rows form the body. aligns separately represents the delimiter row.
|
||||
rows: RowLine[]
|
||||
}
|
||||
|
||||
// ---- 解析与序列化 ----
|
||||
// ---- Parsing and serialization ----
|
||||
|
||||
// 按未转义的 | 切分一行,去掉首尾可选的竖线
|
||||
// Split a line on unescaped | characters, removing optional leading/trailing pipes.
|
||||
function splitRow(line: string): string[] {
|
||||
const cells: string[] = []
|
||||
let cur = ''
|
||||
@@ -100,7 +100,7 @@ function findTable(state: EditorState, pos: number): SyntaxNode | null {
|
||||
return null
|
||||
}
|
||||
|
||||
// ---- 渲染组件 ----
|
||||
// ---- Rendered widget ----
|
||||
|
||||
function renderCell(cell: HTMLElement, raw: string) {
|
||||
cell.innerHTML = renderInlineMarkdown(raw)
|
||||
@@ -109,9 +109,9 @@ function renderCell(cell: HTMLElement, raw: string) {
|
||||
|
||||
interface WidgetState {
|
||||
model: TableModel
|
||||
// 最近编辑的单元格,工具栏操作以它为准
|
||||
// The most recently edited cell, used as the target for toolbar actions.
|
||||
current: { r: number, c: number }
|
||||
// 结构变更后需要聚焦的单元格(DOM 重建后生效)
|
||||
// Cell to focus after a structural change, once the DOM is rebuilt.
|
||||
pendingFocus: { r: number, c: number } | null
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ class TableWidget extends WidgetType {
|
||||
wrap.className = 'cm-rm-table-widget'
|
||||
wrap.lang = this.locale
|
||||
widgetStates.set(wrap, { model: this.model, current: { r: 0, c: 0 }, pendingFocus: null })
|
||||
// 横向滚动放在内层,避免绝对定位的工具栏被裁剪
|
||||
// Keep horizontal scrolling inside so the absolutely positioned toolbar is not clipped.
|
||||
const scroller = document.createElement('div')
|
||||
scroller.className = 'cm-rm-table-scroll'
|
||||
scroller.append(this.buildTable(view, wrap))
|
||||
@@ -159,7 +159,7 @@ class TableWidget extends WidgetType {
|
||||
const sameShape
|
||||
= old.rows.length === this.model.rows.length && old.aligns.join() === this.model.aligns.join()
|
||||
if (!sameShape || st.pendingFocus) {
|
||||
// 重建前记下正在编辑的单元格(如撤销改变了行数),重建后恢复焦点
|
||||
// Remember the active cell before rebuilding, such as after undo changes row count, then restore focus.
|
||||
const editing = dom.querySelector<HTMLElement>('[contenteditable]')
|
||||
const focus
|
||||
= st.pendingFocus ?? (editing ? { r: Number(editing.dataset.r), c: Number(editing.dataset.c) } : null)
|
||||
@@ -173,7 +173,7 @@ class TableWidget extends WidgetType {
|
||||
}
|
||||
return true
|
||||
}
|
||||
// 形状不变:只刷新内容变化的单元格;正在编辑的单元格若与文档一致则不动,以免打断输入
|
||||
// With the same shape, refresh only changed cells; leave the active cell untouched when it matches the document to preserve input.
|
||||
dom.querySelectorAll<HTMLElement>('[data-r]').forEach((cell) => {
|
||||
const r = Number(cell.dataset.r)
|
||||
const c = Number(cell.dataset.c)
|
||||
@@ -226,7 +226,7 @@ class TableWidget extends WidgetType {
|
||||
if (danger) b.className = 'cm-rm-danger'
|
||||
b.innerHTML = svg(icon)
|
||||
b.append(label)
|
||||
// mousedown 阻止默认行为,保持当前单元格的焦点与光标
|
||||
// Prevent default on mousedown to preserve the active cell's focus and cursor.
|
||||
b.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault()
|
||||
run()
|
||||
@@ -266,7 +266,7 @@ function svg(body: string): string {
|
||||
return `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`
|
||||
}
|
||||
|
||||
// 结构变更:基于文档中的最新表格重新生成整张表的源码,返回变更后应聚焦的单元格
|
||||
// For structural changes, regenerate the table's source from the latest document and return the cell to focus afterward.
|
||||
function mutate(
|
||||
view: EditorView,
|
||||
wrap: HTMLElement,
|
||||
@@ -278,14 +278,14 @@ function mutate(
|
||||
const rows = model.rows.map(r => [...r.cells])
|
||||
const aligns = [...model.aligns]
|
||||
st.pendingFocus = f(rows, aligns, st.current)
|
||||
// 结构变更单独成为一步撤销,不与前后的单元格输入合并
|
||||
// Keep structural changes as a separate undo step, not merged with surrounding cell edits.
|
||||
view.dispatch({
|
||||
changes: { from: model.from, to: model.to, insert: serializeTable(aligns, rows) },
|
||||
annotations: isolateHistory.of('full'),
|
||||
})
|
||||
}
|
||||
|
||||
// focusColumn 未指定时聚焦新行的同一列
|
||||
// Without focusColumn, focus the same column in the new row.
|
||||
function insertRow(view: EditorView, wrap: HTMLElement, focusColumn?: number) {
|
||||
return mutate(view, wrap, (rows, aligns, cur) => {
|
||||
rows.splice(cur.r + 1, 0, aligns.map(() => ''))
|
||||
@@ -304,7 +304,7 @@ function insertColumn(view: EditorView, wrap: HTMLElement) {
|
||||
|
||||
function deleteRow(view: EditorView, wrap: HTMLElement) {
|
||||
return mutate(view, wrap, (rows, _, cur) => {
|
||||
// 表头行不能删除
|
||||
// The header row cannot be deleted.
|
||||
if (cur.r === 0) return cur
|
||||
rows.splice(cur.r, 1)
|
||||
return { r: Math.min(cur.r, rows.length - 1), c: cur.c }
|
||||
@@ -339,7 +339,7 @@ function placeCaretAtEnd(el: HTMLElement) {
|
||||
sel.addRange(range)
|
||||
}
|
||||
|
||||
// 单元格编辑时显示该格的 Markdown 源码,失焦后恢复渲染
|
||||
// Show a cell's Markdown source while editing; restore rendered output on blur.
|
||||
function startEdit(cell: HTMLElement, st: WidgetState) {
|
||||
const r = Number(cell.dataset.r)
|
||||
const c = Number(cell.dataset.c)
|
||||
@@ -416,7 +416,7 @@ function bindCell(view: EditorView, wrap: HTMLElement, cell: HTMLElement) {
|
||||
e.preventDefault()
|
||||
moveTo(r() + 1, c())
|
||||
} else if (e.key === 'Escape') {
|
||||
// 退出表格,光标放到表格之后
|
||||
// Exit the table and place the cursor after it.
|
||||
e.preventDefault()
|
||||
const model = currentModel(view, wrap)
|
||||
if (model) view.dispatch({ selection: { anchor: Math.min(model.to + 1, view.state.doc.length) } })
|
||||
@@ -425,14 +425,14 @@ function bindCell(view: EditorView, wrap: HTMLElement, cell: HTMLElement) {
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 装饰 ----
|
||||
// ---- Decorations ----
|
||||
|
||||
// 块级替换装饰跨越多行,只能由 StateField 提供,不能来自 ViewPlugin。
|
||||
// 与其他语法一致:编辑器获得焦点且光标在表格内时才显示源码(见 focus.ts)
|
||||
// Multiline block replacements must come from StateField, not ViewPlugin.
|
||||
// Match other syntax: show source only when the editor is focused and the cursor is inside the table (see focus.ts).
|
||||
function buildTables(state: EditorState): DecorationSet {
|
||||
const out: Range<Decoration>[] = []
|
||||
const sel = state.field(editorFocused) ? state.selection.ranges : []
|
||||
// 只渲染顶层表格;引用、列表内的表格以源码显示
|
||||
// Render only top-level tables; keep tables inside quotes or lists as source.
|
||||
for (let node = syntaxTree(state).topNode.firstChild; node; node = node.nextSibling) {
|
||||
if (node.name !== 'Table') continue
|
||||
if (sel.some(r => r.from <= node.to && r.to >= node.from)) continue
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
|
||||
import { tags as t } from '@lezer/highlight'
|
||||
|
||||
// 编辑器排版样式在 themes/base.css 中以 CSS 变量定义,这里只做语法元素到类名的映射
|
||||
// Editor typography uses CSS variables in common/appearance/base.css; this module maps syntax elements to class names only.
|
||||
const markdownHighlight = HighlightStyle.define([
|
||||
{ tag: t.emphasis, fontStyle: 'italic' },
|
||||
{ tag: t.strong, fontWeight: '600' },
|
||||
|
||||
@@ -32,7 +32,7 @@ export class CheckboxWidget extends WidgetType {
|
||||
el.type = 'checkbox'
|
||||
el.className = 'cm-rm-task'
|
||||
el.checked = this.checked
|
||||
// 用 mousedown 并阻止默认行为:否则编辑器先移动光标,行被展开为源码,点击落空
|
||||
// Use mousedown with preventDefault; otherwise the editor moves the cursor, expands the line to source, and loses the click.
|
||||
el.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault()
|
||||
const pos = view.posAtDOM(el)
|
||||
@@ -65,13 +65,13 @@ const COPY_ICON
|
||||
const CHECK_ICON
|
||||
= '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'
|
||||
|
||||
// 代码块右上角:语言标签(未激活时)+ 复制按钮
|
||||
// Code-block top right: language label when inactive, plus a copy button.
|
||||
export class CodeHeaderWidget extends WidgetType {
|
||||
readonly locale = locale.value
|
||||
constructor(
|
||||
readonly lang: string,
|
||||
readonly showLang: boolean,
|
||||
// 根据组件所在位置取出代码块内容,由调用方结合语法树实现
|
||||
// Read code contents at the widget position; the caller implements this using the syntax tree.
|
||||
readonly codeAt: (view: EditorView, pos: number) => string | null,
|
||||
) {
|
||||
super()
|
||||
@@ -131,12 +131,12 @@ export class ImageWidget extends WidgetType {
|
||||
img.alt = this.alt
|
||||
img.loading = 'lazy'
|
||||
img.decoding = 'async'
|
||||
// 图片加载完成后高度变化,需通知编辑器重新测量,否则光标定位会错位
|
||||
// Image loading changes height; request editor remeasurement to keep cursor positioning correct.
|
||||
img.addEventListener('load', () => view.requestMeasure())
|
||||
// 组件内的事件默认被编辑器忽略,需手动把光标移到图片语法处以展开源码
|
||||
// The editor ignores widget events by default; move the cursor to image syntax manually to expand source.
|
||||
wrap.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault()
|
||||
// 先设光标再聚焦:focus() 会把编辑器选区写回 DOM,反之会读到残留在别处(如表格单元格)的 DOM 选区
|
||||
// Set the cursor before focusing: focus() writes editor selection to the DOM; reversing this reads a stale DOM selection elsewhere, such as a table cell.
|
||||
view.dispatch({ selection: { anchor: view.posAtDOM(wrap) } })
|
||||
view.focus()
|
||||
})
|
||||
@@ -153,9 +153,9 @@ export class ImageWidget extends WidgetType {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 扩展语法(公式、Mermaid、emoji、脚注、目录) ----
|
||||
// ---- Extended syntax (math, Mermaid, emoji, footnotes, table of contents) ----
|
||||
|
||||
// 点击渲染结果时把光标移到源码中(offset 为相对组件起点的位置)以展开编辑,做法同图片
|
||||
// Clicking rendered output moves the cursor into source at offset relative to the widget start, expanding editing as with images.
|
||||
function editOnMousedown(el: HTMLElement, view: EditorView, offset: number) {
|
||||
el.addEventListener('mousedown', (e) => {
|
||||
if (e.button !== 0) return
|
||||
@@ -173,8 +173,8 @@ function showRenderError(el: HTMLElement, err: unknown) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 公式。editOffset 为 null 时是编辑中显示在源码下方的预览(不响应点击,内容变化时原地更新),
|
||||
* 否则是替换源码的渲染结果,点击后光标移到源码 editOffset 处
|
||||
* Math. A null editOffset means a preview below source during editing: no click handling, update in place
|
||||
* on content changes. Otherwise it replaces source; clicking moves the cursor to editOffset in the source.
|
||||
*/
|
||||
export class MathWidget extends WidgetType {
|
||||
readonly locale = locale.value
|
||||
@@ -199,7 +199,7 @@ export class MathWidget extends WidgetType {
|
||||
return el
|
||||
}
|
||||
|
||||
// 只复用预览元素:从渲染结果切到编辑时,编辑器会尝试用新组件更新旧元素(旧元素带点击编辑的监听)
|
||||
// Reuse preview elements only: switching rendered output to editing may update an old element that still has click-to-edit listeners.
|
||||
updateDOM(dom: HTMLElement, view: EditorView) {
|
||||
if (this.editOffset !== null || !dom.classList.contains('cm-rm-preview')) return false
|
||||
this.render(dom, view)
|
||||
@@ -213,7 +213,7 @@ export class MathWidget extends WidgetType {
|
||||
return
|
||||
}
|
||||
el.classList.remove('cm-rm-placeholder')
|
||||
// 加载 KaTeX 前先显示源码
|
||||
// Show source until KaTeX loads.
|
||||
if (!el.firstChild) el.textContent = this.tex
|
||||
void renderMath(el, this.tex, this.display).then(() => view.requestMeasure())
|
||||
}
|
||||
@@ -225,7 +225,7 @@ export class MathWidget extends WidgetType {
|
||||
|
||||
const rendererMounts = new WeakMap<HTMLElement, () => void>()
|
||||
|
||||
/** 通用代码块预览;editOffset 与公式组件使用相同的点击编辑约定。 */
|
||||
/** Generic code-block preview; editOffset follows the math widget's click-to-edit contract. */
|
||||
export class CodeRendererWidget extends WidgetType {
|
||||
readonly locale = locale.value
|
||||
constructor(
|
||||
@@ -306,7 +306,7 @@ export class EmojiWidget extends WidgetType {
|
||||
toDOM(view: EditorView) {
|
||||
const el = document.createElement('span')
|
||||
el.className = 'cm-rm-emoji'
|
||||
// 名称表加载前或名称未知时显示原文
|
||||
// Show original text until the name table loads or when the name is unknown.
|
||||
el.textContent = `:${this.name}:`
|
||||
void emojiFor(this.name).then((e) => {
|
||||
if (e) el.textContent = e
|
||||
@@ -376,7 +376,7 @@ export class TocWidget extends WidgetType {
|
||||
})
|
||||
el.append(item)
|
||||
}
|
||||
// 点击目录空白处展开 [TOC] 源码
|
||||
// Clicking empty table-of-contents space expands [TOC] source.
|
||||
editOnMousedown(el, view, 0)
|
||||
return el
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user