Skip to content

feat: add formula editor#1484

Merged
yanglbme merged 4 commits into
mainfrom
dev/katex
May 25, 2026
Merged

feat: add formula editor#1484
yanglbme merged 4 commits into
mainfrom
dev/katex

Conversation

@YangFong

@YangFong YangFong commented Apr 18, 2026

Copy link
Copy Markdown
Member

close #1483


  • 支持右键菜单/顶部插入菜单进入公式编辑窗口
  • 支持点击预览区公式,进入编辑窗口
  • 确认只提供编辑窗口,不破坏渲染方式

@github-actions

github-actions Bot commented Apr 18, 2026

Copy link
Copy Markdown

🚀 Cloudflare Workers Preview has been successfully deployed!

Preview URL: https://md-pr-1484.doocs.workers.dev

Built with commit 7f64864

@github-actions

github-actions Bot commented Apr 18, 2026

Copy link
Copy Markdown

🚀 Surge Preview has been successfully deployed!

Preview URL: https://doocs-md-preview-pr-1484.surge.sh

Built with commit 7f64864

yanglbme and others added 2 commits May 25, 2026 17:21
- Remove katex npm dependency, use existing window.MathJax instead
- Add renderWithMathJax() helper in FormulaEditorDialog for preview and snippet rendering
- Add escapeHtml to packages/core/src/utils/basicHelpers.ts
- Handle formula click in preview area to open formula editor (useCursorSync)
- Fix mobile scroll: formula library panel now scrollable on small screens
@yanglbme yanglbme marked this pull request as ready for review May 25, 2026 10:55
Copilot AI review requested due to automatic review settings May 25, 2026 10:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a formula editor workflow in the Vue web app while preserving the existing MathJax-based rendering in @md/core. It adds entry points to open a modal dialog for editing/inserting LaTeX and enables opening the editor by clicking formulas in the preview.

Changes:

  • Add a new Formula Editor dialog (LaTeX input + live preview + snippet library) and wire it into the main editor view.
  • Add UI triggers (context menu + insert dropdown) to open the formula editor using the current editor selection.
  • Embed math source metadata into rendered formula HTML (data-math-*) and use it to open the editor from preview clicks.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/core/src/utils/basicHelpers.ts Adds an HTML escaping helper used for safe attribute embedding.
packages/core/src/extensions/katex.ts Adds data-math-* attributes to rendered math nodes to support “click-to-edit” in the web app.
apps/web/vite.config.ts Configures Vue compiler to treat math-field as a custom element.
apps/web/src/views/CodemirrorEditor.vue Mounts the new FormulaEditorDialog in the editor view.
apps/web/src/utils/formula.ts Adds helpers to unwrap/normalize/wrap LaTeX formulas for editor integration.
apps/web/src/stores/ui.ts Adds global UI state/actions for opening/closing the formula editor dialog.
apps/web/src/stores/editor.ts Adds replaceText() helper used by the formula editor to update existing source.
apps/web/src/composables/useCursorSync.ts Opens the formula editor when clicking rendered math in the preview.
apps/web/src/components/editor/FormulaEditorDialog.vue Implements the formula editor modal UI and insertion/editing behavior.
apps/web/src/components/editor/EditorContextMenu.vue Adds a context-menu entry to open the formula editor.
apps/web/src/components/editor/editor-header/InsertDropdown.vue Adds a header insert-menu entry to open the formula editor.

Comment on lines +89 to +96
function openFormulaEditor() {
const selection = normalizeFormulaInput(editorStore.getSelection())
uiStore.openFormulaEditor({
value: selection.latex,
displayMode: selection.displayMode,
sourceRaw: selection.sourceRaw,
})
}
Comment on lines +19 to +26
function openFormulaEditor() {
const selection = normalizeFormulaInput(editorStore.getSelection())
uiStore.openFormulaEditor({
value: selection.latex,
displayMode: selection.displayMode,
sourceRaw: selection.sourceRaw,
})
}
Comment on lines +54 to +68
export function normalizeFormulaInput(text: string): FormulaInput {
const trimmed = text.trim()
if (!trimmed) {
return { latex: ``, displayMode: false, sourceRaw: null }
}

const unwrapped = unwrapFormula(trimmed)
const isWrapped = unwrapped.latex !== trimmed || unwrapped.displayMode || /^(\$\$|\\\[|\\\(|\$)/.test(trimmed)

return {
latex: unwrapped.latex,
displayMode: unwrapped.displayMode || trimmed.includes(`\n`),
sourceRaw: isWrapped ? trimmed : null,
}
}
Comment on lines +67 to +81
const replaceText = (oldText: string, newText: string) => {
if (!editor.value || !oldText)
return false

const content = editor.value.state.doc.toString()
const from = content.indexOf(oldText)
if (from === -1)
return false

editor.value.dispatch({
changes: { from, to: from + oldText.length, insert: newText },
})
editor.value.focus()
return true
}
Comment on lines +170 to +185
function renderWithMathJax(latex: string, display: boolean): string {
try {
// @ts-expect-error MathJax is a global variable
window.MathJax.texReset()
// @ts-expect-error MathJax is a global variable
const mjxContainer = window.MathJax.tex2svg(latex, { display })
const svg = mjxContainer.firstChild as SVGElement
svg.style.display = display ? `block` : `initial`
svg.style.setProperty(`max-width`, `300vw`, `important`)
svg.style.flexShrink = `0`
return svg.outerHTML
}
catch {
return `<div class="text-sm text-destructive break-all">${escapeHtml(latex)}</div>`
}
}
class="rounded-lg border bg-background px-3 py-2 text-left text-sm hover:border-primary hover:bg-primary/5 transition-colors"
@click="insertSnippet(snippet)"
>
<span class="flex items-center overflow-x-auto whitespace-nowrap font-mono h-15" v-html="renderWithMathJax(snippet, false)" />
Comment thread packages/core/src/extensions/katex.ts Outdated
Comment on lines +40 to +45
if (!display) {
// 新主题系统:使用 class 而非内联样式
return `<span class="katex-inline">${svg.outerHTML}</span>`
return `<span class="katex-inline" data-math-latex="${escapeHtml(token.text)}" data-math-display="false" data-math-raw="${escapeHtml(token.raw ?? token.text)}">${svg.outerHTML}</span>`
}

return `<section class="katex-block">${svg.outerHTML}</section>`
return `<section class="katex-block" data-math-latex="${escapeHtml(token.text)}" data-math-display="true" data-math-raw="${escapeHtml(token.raw ?? token.text)}">${svg.outerHTML}</section>`
- EditorContextMenu/InsertDropdown: don't pass sourceRaw for selection-based editing, use replaceSelection() instead
- formula.ts: fix isWrapped detection to only check if unwrapFormula actually stripped delimiters
- editor.ts: replaceText() now replaces the occurrence nearest to the current cursor
- FormulaEditorDialog: cache MathJax snippet renders to avoid redundant re-renders
- katex.ts: remove unused data-math-latex attribute to reduce HTML output size
@yanglbme yanglbme merged commit c5da3b4 into main May 25, 2026
2 checks passed
@github-actions

Copy link
Copy Markdown

🗑️ Cloudflare Workers preview deployment has been cleaned up.

@yanglbme yanglbme deleted the dev/katex branch May 25, 2026 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

希望支持公式编辑器

3 participants