feat(editor): add visual formula editing with MathLive
Add an on-demand formula keyboard, editable placeholder styling, and LaTeX source switching. Expand MathLive macros for KaTeX compatibility and prevent applying incomplete formulas. Refs #514
This commit is contained in:
1 parent
e13691749f
commit
d42fbec67e
14 files changed
+365
-54
No files matched your search
@@ -32,6 +32,7 @@
|
||||
"html-to-image": "^1.11.13",
|
||||
"lowlight": "^3.3.0",
|
||||
"lucide-react": "1.21.0",
|
||||
"mathlive": "0.111.1",
|
||||
"mediabunny": "^1.61.3",
|
||||
"mermaid": "^11.16.0",
|
||||
"motion": "^12.43.0",
|
||||
|
||||
@@ -401,6 +401,16 @@ export const EditorToolbar = ({
|
||||
<MemoEditorToolbarDivider className="hidden sm:block" />
|
||||
</>
|
||||
)}
|
||||
{showFormattingTools && onPickMathFormula && (
|
||||
<EditorToolbarButton
|
||||
title={t("editorToolbar.math")}
|
||||
active={isActive("inlineMath") || isActive("blockMath")}
|
||||
disabled={disabled}
|
||||
onClick={onPickMathFormula}
|
||||
>
|
||||
<Sigma className="h-4 w-4" />
|
||||
</EditorToolbarButton>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -575,16 +585,6 @@ export const EditorToolbar = ({
|
||||
>
|
||||
<ChartNoAxesCombined className="h-4 w-4" />
|
||||
</EditorToolbarButton>
|
||||
{onPickMathFormula && (
|
||||
<EditorToolbarButton
|
||||
title={t("editorToolbar.math")}
|
||||
active={isActive("inlineMath") || isActive("blockMath")}
|
||||
disabled={disabled}
|
||||
onClick={onPickMathFormula}
|
||||
>
|
||||
<Sigma className="h-4 w-4" />
|
||||
</EditorToolbarButton>
|
||||
)}
|
||||
<EditorToolbarButton
|
||||
title={t("editorToolbar.horizontalRule")}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -6,27 +6,13 @@ import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { MathFormulaDraft, MathFormulaKind } from "@/components/editor/math-formula";
|
||||
|
||||
const renderPreviewHtml = (latex: string, kind: MathFormulaKind) => {
|
||||
const trimmed = latex.trim();
|
||||
if (!trimmed) return "";
|
||||
try {
|
||||
return katex.renderToString(trimmed, {
|
||||
displayMode: kind === "block",
|
||||
throwOnError: false,
|
||||
strict: "warn",
|
||||
trust: false,
|
||||
});
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
import { MathVisualInput } from "@/components/editor/MathVisualInput";
|
||||
import { hasMathPlaceholders, renderMathPreview } from "@/components/editor/math-formula-preview";
|
||||
|
||||
export const MathFormulaDialog = ({
|
||||
open,
|
||||
@@ -41,13 +27,14 @@ export const MathFormulaDialog = ({
|
||||
onApply: (draft: MathFormulaDraft) => void;
|
||||
onRemove?: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const latexId = useId();
|
||||
const previewId = useId();
|
||||
const latexInputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [kind, setKind] = useState<MathFormulaKind>(draft?.kind ?? "inline");
|
||||
const [latex, setLatex] = useState(draft?.latex ?? "");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [inputMode, setInputMode] = useState<"visual" | "source">("visual");
|
||||
const editing = typeof draft?.pos === "number";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,20 +42,28 @@ export const MathFormulaDialog = ({
|
||||
setKind(draft?.kind ?? "inline");
|
||||
setLatex(draft?.latex ?? "");
|
||||
setError(null);
|
||||
const timer = window.setTimeout(() => {
|
||||
latexInputRef.current?.focus();
|
||||
latexInputRef.current?.select();
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
setInputMode("visual");
|
||||
}, [draft, open]);
|
||||
|
||||
const previewHtml = useMemo(() => renderPreviewHtml(latex, kind), [kind, latex]);
|
||||
useEffect(() => {
|
||||
if (open && inputMode === "source") latexInputRef.current?.focus();
|
||||
}, [inputMode, open]);
|
||||
|
||||
const previewHtml = useMemo(() => renderMathPreview(latex, kind), [kind, latex]);
|
||||
const incomplete = hasMathPlaceholders(latex);
|
||||
|
||||
const submit = () => {
|
||||
if (incomplete) return;
|
||||
if (!latex.trim()) {
|
||||
setError(t("mathFormulaDialog.errorEmpty"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
katex.renderToString(latex.trim(), { throwOnError: true, trust: false, strict: "warn" });
|
||||
} catch {
|
||||
setError(t("mathFormulaDialog.previewError"));
|
||||
return;
|
||||
}
|
||||
onApply({
|
||||
kind,
|
||||
latex,
|
||||
@@ -81,15 +76,12 @@ export const MathFormulaDialog = ({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg gap-0 overflow-hidden p-0">
|
||||
<DialogContent aria-describedby={undefined} className="max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-2xl gap-0 overflow-y-auto p-0">
|
||||
<DialogHeader className="border-b border-slate-200 px-5 py-5 pr-12 text-left">
|
||||
<DialogTitle className="flex items-center gap-2 text-sm leading-6">
|
||||
<Sigma className="h-5 w-5 text-slate-700" />
|
||||
{editing ? t("mathFormulaDialog.editTitle") : t("mathFormulaDialog.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="pt-1 leading-5">
|
||||
{t("mathFormulaDialog.description")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form
|
||||
@@ -107,7 +99,7 @@ export const MathFormulaDialog = ({
|
||||
type="button"
|
||||
className={
|
||||
kind === value
|
||||
? "flex-1 rounded-sm bg-workspace-selection px-3 py-1.5 text-xs font-semibold text-slate-950"
|
||||
? "flex-1 rounded-sm bg-slate-100 px-3 py-1.5 text-xs font-semibold text-slate-950"
|
||||
: "flex-1 rounded-sm px-3 py-1.5 text-xs font-medium text-slate-600 hover:bg-slate-50"
|
||||
}
|
||||
aria-pressed={kind === value}
|
||||
@@ -120,9 +112,29 @@ export const MathFormulaDialog = ({
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor={latexId} className="text-xs font-medium text-slate-600">
|
||||
{t("mathFormulaDialog.latexLabel")}
|
||||
</label>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<label htmlFor={latexId} className="text-xs font-medium text-slate-600">
|
||||
{t(inputMode === "visual" ? "mathFormulaDialog.visualLabel" : "mathFormulaDialog.latexLabel")}
|
||||
</label>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => {
|
||||
setInputMode(inputMode === "visual" ? "source" : "visual");
|
||||
setError(null);
|
||||
}}>
|
||||
{t(inputMode === "visual" ? "mathFormulaDialog.sourceMode" : "mathFormulaDialog.visualMode")}
|
||||
</Button>
|
||||
</div>
|
||||
{inputMode === "visual" ? (open ? (
|
||||
<MathVisualInput
|
||||
id={latexId}
|
||||
value={latex}
|
||||
label={t("mathFormulaDialog.visualLabel")}
|
||||
loadingLabel={t("mathFormulaDialog.loading")}
|
||||
language={i18n.resolvedLanguage ?? i18n.language}
|
||||
hint={incomplete ? t("mathFormulaDialog.incomplete") : undefined}
|
||||
onChange={(value) => { setLatex(value); setError(null); }}
|
||||
onLoadError={() => { setInputMode("source"); setError(t("mathFormulaDialog.loadError")); }}
|
||||
/>
|
||||
) : null) : (
|
||||
<textarea
|
||||
ref={latexInputRef}
|
||||
id={latexId}
|
||||
@@ -137,6 +149,8 @@ export const MathFormulaDialog = ({
|
||||
if (error) setError(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{inputMode === "source" && incomplete && <p className="text-xs leading-5 text-slate-500" role="status">{t("mathFormulaDialog.incomplete")}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
@@ -189,7 +203,7 @@ export const MathFormulaDialog = ({
|
||||
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" variant="solid">
|
||||
<Button type="submit" variant="solid" disabled={incomplete}>
|
||||
{t("mathFormulaDialog.apply")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { MathfieldElement } from "mathlive";
|
||||
import "./math-visual-input.css";
|
||||
import placeholderStyles from "./mathlive-placeholder.css?inline";
|
||||
|
||||
export const MathVisualInput = ({
|
||||
id, value, label, loadingLabel, language, hint, onChange, onLoadError,
|
||||
}: {
|
||||
id: string;
|
||||
value: string;
|
||||
label: string;
|
||||
loadingLabel: string;
|
||||
language: string;
|
||||
hint?: string;
|
||||
onChange: (value: string) => void;
|
||||
onLoadError: () => void;
|
||||
}) => {
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
const keyboardHostRef = useRef<HTMLDivElement>(null);
|
||||
const fieldRef = useRef<MathfieldElement | null>(null);
|
||||
const latestRef = useRef({ value, onChange, onLoadError });
|
||||
latestRef.current = { value, onChange, onLoadError };
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let dispose: (() => void) | undefined;
|
||||
void import("./mathlive-loader").then(({ MathfieldElement, convertLatexToMarkup }) => {
|
||||
const host = hostRef.current;
|
||||
const keyboardHost = keyboardHostRef.current;
|
||||
if (cancelled || !host || !keyboardHost) return;
|
||||
MathfieldElement.locale = language.toLowerCase();
|
||||
const field = new MathfieldElement();
|
||||
field.id = id;
|
||||
field.setAttribute("aria-label", label);
|
||||
field.mathVirtualKeyboardPolicy = "manual";
|
||||
field.placeholderSymbol = "\u25a2";
|
||||
field.setValue(latestRef.current.value, { silenceNotifications: true });
|
||||
// Expand MathLive-specific macros (e.g. imaginaryI) into portable LaTeX
|
||||
// before passing it to KaTeX or persisting it in the document.
|
||||
const handleInput = () => latestRef.current.onChange(field.getValue("latex-expanded"));
|
||||
field.addEventListener("input", handleInput);
|
||||
host.appendChild(field);
|
||||
const placeholderStyle = document.createElement("style");
|
||||
placeholderStyle.textContent = placeholderStyles;
|
||||
field.shadowRoot?.appendChild(placeholderStyle);
|
||||
// The pinned MathLive version emits slots as ordinary glyph spans rather
|
||||
// than exposing a CSS part. Mark only its dedicated placeholder symbol.
|
||||
const markInputSlots = () => {
|
||||
field.shadowRoot?.querySelectorAll("span[data-atom-id]").forEach((element) => {
|
||||
if (element.children.length === 0 && element.textContent === "\u25a2") {
|
||||
element.classList.add("edgeever-math-input-slot");
|
||||
}
|
||||
});
|
||||
};
|
||||
const slotObserver = new MutationObserver(markInputSlots);
|
||||
if (field.shadowRoot) slotObserver.observe(field.shadowRoot, { childList: true, subtree: true });
|
||||
markInputSlots();
|
||||
field.menuItems = [];
|
||||
fieldRef.current = field;
|
||||
if (field.getValue("latex-expanded") !== latestRef.current.value) handleInput();
|
||||
|
||||
// Keep the keyboard inside Radix's modal focus/pointer boundary.
|
||||
const keyboard = window.mathVirtualKeyboard;
|
||||
const originalLayouts = keyboard.layouts;
|
||||
const keyLabels: Record<string, string> = {
|
||||
"#@^2}": "\\square^2",
|
||||
"#@^{#0}}": "\\square^{\\square}",
|
||||
"\\sqrt{#0}": "\\sqrt{\\square}",
|
||||
};
|
||||
keyboard.layouts = keyboard.normalizedLayouts.map((layout) => ({
|
||||
...layout,
|
||||
layers: layout.layers.map((layer) => ({
|
||||
...layer,
|
||||
rows: layer.rows?.map((row) => row.map((key) => {
|
||||
const label = key.latex && keyLabels[key.latex];
|
||||
return label ? { ...key, label: convertLatexToMarkup(label) } : key;
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
keyboard.container = keyboardHost;
|
||||
const resizeKeyboard = () => {
|
||||
keyboardHost.style.height = `${keyboard.boundingRect.height}px`;
|
||||
};
|
||||
keyboard.addEventListener("geometrychange", resizeKeyboard);
|
||||
dispose = () => {
|
||||
slotObserver.disconnect();
|
||||
keyboard.removeEventListener("geometrychange", resizeKeyboard);
|
||||
keyboard.hide();
|
||||
keyboard.layouts = originalLayouts;
|
||||
keyboard.container = null;
|
||||
field.removeEventListener("input", handleInput);
|
||||
field.remove();
|
||||
fieldRef.current = null;
|
||||
};
|
||||
setReady(true);
|
||||
field.focus();
|
||||
keyboard.show();
|
||||
resizeKeyboard();
|
||||
}).catch((error: unknown) => {
|
||||
dispose?.();
|
||||
console.error("Failed to load the formula editor", error);
|
||||
if (!cancelled) latestRef.current.onLoadError();
|
||||
});
|
||||
return () => { cancelled = true; dispose?.(); };
|
||||
}, [id, label, language]);
|
||||
|
||||
useEffect(() => {
|
||||
const field = fieldRef.current;
|
||||
if (field && field.getValue("latex-expanded") !== value) field.setValue(value, { silenceNotifications: true });
|
||||
}, [value, ready]);
|
||||
|
||||
return (
|
||||
<div className="edgeever-math-visual-input space-y-2">
|
||||
{!ready && <p role="status" className="text-xs text-slate-500">{loadingLabel}</p>}
|
||||
<div ref={hostRef} />
|
||||
{hint && <p role="status" className="text-xs leading-5 text-slate-500">{hint}</p>}
|
||||
<div ref={keyboardHostRef} className="relative w-full overflow-hidden rounded-md" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { hasMathPlaceholders, renderMathPreview } from "./math-formula-preview.ts";
|
||||
|
||||
test("MathLive's unfinished square and root render without becoming valid saved formulas", () => {
|
||||
for (const latex of ["e=\\placeholder{}^2", "x^{\\placeholder{}}", "\\sqrt{\\placeholder{}}"] ) {
|
||||
expect(hasMathPlaceholders(latex)).toBe(true);
|
||||
expect(renderMathPreview(latex, "inline")).toContain('class="katex"');
|
||||
}
|
||||
});
|
||||
|
||||
test("completed slots render normally and unknown commands still fail", () => {
|
||||
expect(hasMathPlaceholders("e=x^2")).toBe(false);
|
||||
expect(renderMathPreview("e=x^2", "inline")).toContain('class="katex"');
|
||||
expect(renderMathPreview("\\notARealCommand{}", "inline")).toBe("");
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import katex from "katex";
|
||||
import type { MathFormulaKind } from "./math-formula";
|
||||
|
||||
export const hasMathPlaceholders = (latex: string) => /\\placeholder\b/.test(latex);
|
||||
|
||||
export const renderMathPreview = (latex: string, kind: MathFormulaKind) => {
|
||||
if (!latex.trim()) return "";
|
||||
try {
|
||||
return katex.renderToString(latex.trim(), {
|
||||
displayMode: kind === "block",
|
||||
throwOnError: true,
|
||||
strict: "warn",
|
||||
trust: false,
|
||||
// MathLive emits this command for slots still being edited. It is only
|
||||
// supported in the preview; unfinished formulas must not be saved.
|
||||
macros: { "\\placeholder": "\\boxed{\\phantom{x#1}}" },
|
||||
});
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
.edgeever-math-visual-input {
|
||||
--keyboard-accent-color: rgb(var(--slate-900-rgb));
|
||||
--keyboard-background: hsl(var(--card));
|
||||
--keycap-height: 36px;
|
||||
--keycap-font-size: 18px;
|
||||
--keycap-gap: 2px;
|
||||
--keyboard-padding-top: 0px;
|
||||
--keyboard-padding-bottom: 0px;
|
||||
--keyboard-toolbar-background: hsl(var(--card));
|
||||
--keyboard-toolbar-text: rgb(var(--slate-600-rgb));
|
||||
--keycap-background: rgb(var(--slate-100-rgb));
|
||||
--keycap-background-hover: rgb(var(--slate-200-rgb));
|
||||
--keycap-background-active: rgb(var(--slate-200-rgb));
|
||||
--keycap-background-pressed: rgb(var(--slate-200-rgb));
|
||||
--keycap-text: rgb(var(--slate-900-rgb));
|
||||
--keycap-text-active: rgb(var(--slate-900-rgb));
|
||||
--keycap-text-pressed: rgb(var(--slate-900-rgb));
|
||||
--keycap-border: rgb(var(--slate-200-rgb));
|
||||
--keycap-border-bottom: rgb(var(--slate-200-rgb));
|
||||
--keycap-primary-background: rgb(var(--slate-100-rgb));
|
||||
--keycap-primary-background-hover: rgb(var(--slate-200-rgb));
|
||||
--keycap-primary-text: rgb(var(--slate-900-rgb));
|
||||
--keycap-secondary-background: rgb(var(--slate-100-rgb));
|
||||
--keycap-secondary-background-hover: rgb(var(--slate-200-rgb));
|
||||
--keycap-secondary-text: rgb(var(--slate-900-rgb));
|
||||
--keycap-secondary-border: rgb(var(--slate-200-rgb));
|
||||
--keycap-secondary-border-bottom: rgb(var(--slate-200-rgb));
|
||||
--box-placeholder-color: rgb(var(--slate-500-rgb));
|
||||
--variant-panel-background: hsl(var(--card));
|
||||
--variant-keycap-background-active: rgb(var(--slate-200-rgb));
|
||||
}
|
||||
|
||||
/* MathLive's default modifier padding assumes taller keys. Keep icons centered
|
||||
in our compact keys, including Shift and Backspace's edge-aligned defaults. */
|
||||
.edgeever-math-visual-input .ML__keyboard .MLK__rows .MLK__row > .action,
|
||||
.edgeever-math-visual-input .ML__keyboard .MLK__rows .MLK__row > .shift {
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.edgeever-math-visual-input .ML__keyboard .MLK__rows .MLK__row > .action > svg,
|
||||
.edgeever-math-visual-input .ML__keyboard .MLK__rows .MLK__row > .shift > svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edgeever-math-visual-input math-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
border: 1px solid rgb(var(--slate-200-rgb));
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
background: hsl(var(--card));
|
||||
color: rgb(var(--slate-900-rgb));
|
||||
font-size: 22px;
|
||||
--caret-color: rgb(var(--slate-900-rgb));
|
||||
--selection-background-color: rgb(var(--slate-200-rgb));
|
||||
}
|
||||
|
||||
.edgeever-math-visual-input math-field:focus-within {
|
||||
border-color: rgb(var(--slate-500-rgb));
|
||||
outline: 2px solid rgb(var(--slate-900-rgb) / 10%);
|
||||
}
|
||||
|
||||
.edgeever-math-visual-input math-field::part(virtual-keyboard-toggle) {
|
||||
color: rgb(var(--slate-500-rgb));
|
||||
}
|
||||
|
||||
.edgeever-math-visual-input math-field::part(menu-toggle) {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { MathfieldElement } from "mathlive";
|
||||
import "mathlive/fonts.css";
|
||||
|
||||
// Vite packages the fonts locally. Disable MathLive's automatic directory/CDN
|
||||
// loading and optional key sounds so the editor also works in desktop/offline use.
|
||||
MathfieldElement.fontsDirectory = null;
|
||||
MathfieldElement.soundsDirectory = null;
|
||||
|
||||
export { MathfieldElement, convertLatexToMarkup } from "mathlive";
|
||||
@@ -0,0 +1,31 @@
|
||||
/* MathLive does not expose expression placeholders as a CSS part. This sheet
|
||||
lives inside this math-field's shadow root and only styles empty input slots. */
|
||||
.edgeever-math-input-slot {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
outline: 1px dashed rgb(var(--slate-500-rgb));
|
||||
border-radius: 3px;
|
||||
background: rgb(var(--slate-50-rgb));
|
||||
/* MathLive uses !important for selected glyph colors. Hide the square glyph
|
||||
while preserving its layout and the editor's selection/hit testing. */
|
||||
color: transparent !important;
|
||||
opacity: 1;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.edgeever-math-input-slot.ML__selected,
|
||||
.ML__placeholder-selected {
|
||||
outline-color: rgb(var(--slate-700-rgb));
|
||||
background: rgb(var(--slate-100-rgb));
|
||||
}
|
||||
|
||||
.ML__focused .edgeever-math-input-slot.ML__selected::after,
|
||||
.ML__focused .ML__placeholder-selected::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 15%;
|
||||
bottom: 15%;
|
||||
left: 50%;
|
||||
border-left: 1px solid rgb(var(--slate-900-rgb));
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -250,6 +250,7 @@
|
||||
"html-to-image": "^1.11.13",
|
||||
"lowlight": "^3.3.0",
|
||||
"lucide-react": "1.21.0",
|
||||
"mathlive": "0.111.1",
|
||||
"mediabunny": "^1.61.3",
|
||||
"mermaid": "^11.16.0",
|
||||
"motion": "^12.43.0",
|
||||
@@ -3072,6 +3073,8 @@
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mathlive": ["mathlive@0.111.1", "", {}, "sha512-RVaC7NO02Lmx3ZNUxOHR2xCRpcvTfBb2BtdgV60cTanAMufaUkWcPPB2VxChcsNeL+0rTQKqOPDYxXLPOXJI+A=="],
|
||||
|
||||
"md5": ["md5@2.3.0", "", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g=="],
|
||||
|
||||
"mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
|
||||
|
||||
@@ -2456,17 +2456,23 @@ export const enUS = {
|
||||
mathFormulaDialog: {
|
||||
title: "Insert formula",
|
||||
editTitle: "Edit formula",
|
||||
description: "Write the formula in LaTeX. Inline math sits in a sentence; a display formula gets its own line.",
|
||||
description: "Build formulas visually or edit LaTeX. Inline formulas stay in the sentence; display formulas occupy their own line.",
|
||||
inline: "Inline",
|
||||
block: "Display",
|
||||
latexLabel: "LaTeX",
|
||||
latexPlaceholder: "E = mc^2",
|
||||
preview: "Preview",
|
||||
previewEmpty: "Preview appears after you enter LaTeX",
|
||||
previewEmpty: "Enter a formula to preview it",
|
||||
previewError: "Could not render this formula",
|
||||
apply: "Apply",
|
||||
remove: "Delete formula",
|
||||
errorEmpty: "Enter LaTeX.",
|
||||
errorEmpty: "Please enter a formula.",
|
||||
visualLabel: "Formula",
|
||||
sourceMode: "LaTeX source",
|
||||
visualMode: "Visual editor",
|
||||
loading: "Loading formula editor…",
|
||||
loadError: "The visual editor could not load. You can continue with LaTeX.",
|
||||
incomplete: "Type in an empty slot; press Tab for the next one",
|
||||
},
|
||||
noteLinkPicker: {
|
||||
title: "Link a note",
|
||||
|
||||
@@ -2456,17 +2456,23 @@ export const ja = {
|
||||
mathFormulaDialog: {
|
||||
title: "数式を挿入",
|
||||
editTitle: "数式を編集",
|
||||
description: "LaTeX で数式を書きます。インラインは文中に入り、ブロックは独立した行になります。",
|
||||
description: "記号キーボードで数式を作成するか、LaTeX を編集できます。インライン数式は文中に、ブロック数式は独立した行に表示されます。",
|
||||
inline: "インライン",
|
||||
block: "ブロック",
|
||||
latexLabel: "LaTeX",
|
||||
latexPlaceholder: "E = mc^2",
|
||||
preview: "プレビュー",
|
||||
previewEmpty: "LaTeX を入力するとプレビューされます",
|
||||
previewEmpty: "数式を入力するとプレビューされます",
|
||||
previewError: "この数式を表示できません",
|
||||
apply: "適用",
|
||||
remove: "数式を削除",
|
||||
errorEmpty: "LaTeX を入力してください。",
|
||||
errorEmpty: "数式を入力してください。",
|
||||
visualLabel: "数式",
|
||||
sourceMode: "LaTeX ソース",
|
||||
visualMode: "ビジュアル編集",
|
||||
loading: "数式エディターを読み込み中…",
|
||||
loadError: "ビジュアルエディターを読み込めませんでした。LaTeX で編集できます。",
|
||||
incomplete: "空欄に入力し、Tab キーで次の空欄へ移動",
|
||||
},
|
||||
noteLinkPicker: {
|
||||
title: "ノートをリンク",
|
||||
|
||||
@@ -2456,17 +2456,23 @@ export const pl = {
|
||||
mathFormulaDialog: {
|
||||
title: "Wstaw wzór",
|
||||
editTitle: "Edytuj wzór",
|
||||
description: "Zapisz wzór w LaTeX. Wzór w tekście jest częścią zdania, a wzór wyróżniony zajmuje osobny wiersz.",
|
||||
description: "Twórz wzory za pomocą klawiatury symboli lub edytuj LaTeX. Wzory liniowe pozostają w zdaniu, a blokowe zajmują osobny wiersz.",
|
||||
inline: "W tekście",
|
||||
block: "Wyróżniony",
|
||||
latexLabel: "LaTeX",
|
||||
latexPlaceholder: "E = mc^2",
|
||||
preview: "Podgląd",
|
||||
previewEmpty: "Podgląd pojawi się po wpisaniu kodu LaTeX",
|
||||
previewEmpty: "Wpisz wzór, aby zobaczyć podgląd",
|
||||
previewError: "Nie udało się wyrenderować tego wzoru",
|
||||
apply: "Zastosuj",
|
||||
remove: "Usuń wzór",
|
||||
errorEmpty: "Wpisz kod LaTeX.",
|
||||
errorEmpty: "Wpisz wzór.",
|
||||
visualLabel: "Wzór",
|
||||
sourceMode: "Kod LaTeX",
|
||||
visualMode: "Edytor wizualny",
|
||||
loading: "Ładowanie edytora wzorów…",
|
||||
loadError: "Nie udało się załadować edytora wizualnego. Możesz użyć LaTeX.",
|
||||
incomplete: "Wpisz w pustym polu; Tab przechodzi do następnego",
|
||||
},
|
||||
noteLinkPicker: {
|
||||
title: "Połącz z notatką",
|
||||
|
||||
@@ -2454,17 +2454,23 @@ export const zhCN = {
|
||||
mathFormulaDialog: {
|
||||
title: "插入公式",
|
||||
editTitle: "编辑公式",
|
||||
description: "用 LaTeX 编写公式。行内公式会出现在句子中,块级公式单独占一行。",
|
||||
description: "使用符号键盘直观编写公式,也可编辑 LaTeX。行内公式出现在句子中,块级公式单独占一行。",
|
||||
inline: "行内",
|
||||
block: "块级",
|
||||
latexLabel: "LaTeX",
|
||||
latexPlaceholder: "E = mc^2",
|
||||
preview: "预览",
|
||||
previewEmpty: "输入 LaTeX 后显示预览",
|
||||
previewEmpty: "输入公式后显示预览",
|
||||
previewError: "无法渲染该公式",
|
||||
apply: "应用",
|
||||
remove: "删除公式",
|
||||
errorEmpty: "请输入 LaTeX。",
|
||||
errorEmpty: "请输入公式。",
|
||||
visualLabel: "公式",
|
||||
sourceMode: "LaTeX 源码",
|
||||
visualMode: "可视化编辑",
|
||||
loading: "正在加载公式编辑器…",
|
||||
loadError: "可视化编辑器暂时无法加载,可继续使用 LaTeX。",
|
||||
incomplete: "在空框中输入,按 Tab 切换到下一处",
|
||||
},
|
||||
noteLinkPicker: {
|
||||
title: "引用笔记",
|
||||
|
||||
Reference in new issue
Block a user