Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 29 additions & 21 deletions components/notes/InlineMarkdownEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,27 @@ test("source undo history coalesces only adjacent edits of the same typing kind"
});

test("math language detection does not mistake plain text blocks for TeX", () => {
assert.equal(isNoteMathLanguageLabel("math"), false);
assert.equal(isNoteMathLanguageLabel("Math (LaTeX)"), false);
assert.equal(isNoteMathLanguageLabel("math"), true);
assert.equal(isNoteMathLanguageLabel("Math (LaTeX)"), true);
assert.equal(isNoteMathLanguageLabel("language-latex"), true);
assert.equal(isNoteMathLanguageLabel("language-tex highlighted"), true);
assert.equal(isNoteMathLanguageLabel("latex"), true);
assert.equal(isNoteMathLanguageLabel("tex"), true);
assert.equal(isNoteMathLanguageLabel("公式"), false);
assert.equal(isNoteMathLanguageLabel("katex"), true);
assert.equal(isNoteMathLanguageLabel("formula"), true);
assert.equal(isNoteMathLanguageLabel("公式"), true);
assert.equal(isNoteMathLanguageLabel("text"), false);
assert.equal(isNoteMathLanguageLabel("plaintext"), false);
assert.equal(isNoteMathLanguageLabel("typescript"), false);
assert.equal(shouldRenderNoteMathFormula("Plain text"), false);
assert.equal(shouldRenderNoteMathFormula("plaintext"), false);
assert.equal(shouldRenderNoteMathFormula(""), false);
assert.equal(shouldRenderNoteMathFormula("math"), false);
assert.equal(shouldRenderNoteMathFormula("math"), true);
assert.equal(shouldRenderNoteMathFormula("latex"), true);
assert.equal(shouldRenderNoteMathFormula("tex"), true);
assert.equal(shouldRenderNoteMathFormula("katex"), true);
assert.equal(shouldRenderNoteMathFormula("plaintext", "$$ x + y = z $$"), true);
assert.equal(shouldRenderNoteMathFormula("plaintext", "hello world"), false);
});

test("live note decoration scans are debounced while preview mounts stay immediate", () => {
Expand Down Expand Up @@ -368,8 +373,7 @@ test("note editor registers a code block editor for pasted fenced code", () => {
/codeBlockPlugin\([^)]*\),\s*codeMirrorPlugin\(\{\s*codeBlockLanguages:/s,
);
assert.match(source, /codeMirrorExtensions:\s*NOTE_CODE_MIRROR_EXTENSIONS/);
assert.match(source, /syntaxHighlighting\(noteCodeHighlightStyle\)/);
});
assert.match(source, /syntaxHighlighting\(noteCodeHighlightStyle\)/); assert.match(source, /tooltips\(\{\s*parent:\s*typeof document !== "undefined" \? document\.body : undefined,\s*\}\)/);});

test("note editor enables image plugin for remote markdown images", () => {
const source = readFileSync(new URL("./InlineMarkdownEditor.tsx", import.meta.url), "utf8");
Expand Down Expand Up @@ -510,26 +514,30 @@ test("note code block active line is highlighted only while focused", () => {
styles,
/\.netcatty-mdx-editor\s+\.cm-editor:focus-within\s+\.cm-activeLine,\s*\.netcatty-mdx-editor\s+\.cm-editor:focus-within\s+\.cm-activeLineGutter\s*\{[^}]*background:\s*hsl\(var\(--primary\)\s*\/\s*0\.08\)/s,
);
});

test("note code block frame is borderless and language picker is compact", () => {
const styles = readFileSync(new URL("../../index.css", import.meta.url), "utf8");

assert.match(
styles,
/\.netcatty-mdx-editor\s+\[class\*="_codeMirrorWrapper_"\]\s*\{[^}]*border:\s*0\s*!important;[^}]*background:\s*transparent\s*!important;[^}]*padding:\s*0\s*!important;/s,
/\.cm-tooltip,\s*\.netcatty-mdx-editor\s+\.cm-tooltip\s*\{[^}]*background:\s*hsl\(var\(--popover\)\)/s,
);
assert.match(
styles,
/\.cm-tooltip\.cm-tooltip-autocomplete\s*>\s*ul\s*\{[^}]*background:\s*hsl\(var\(--popover\)\)/s,
);
assert.match(
styles,
/\.netcatty-mdx-editor\s+\.cm-editor\s*\{[^}]*border:\s*0\s*!important;[^}]*background:\s*transparent\s*!important;/s,
/\.cm-tooltip\s+\.cm-completionMatchedText,\s*\.netcatty-mdx-editor\s+\.cm-tooltip\s+\.cm-completionMatchedText\s*\{[^}]*color:\s*inherit\s*!important;[^}]*font-weight:\s*700\s*!important;[^}]*text-decoration:\s*underline\s*!important;/s,
);
});

test("note code block frame is borderless and language picker is compact", () => {
const styles = readFileSync(new URL("../../index.css", import.meta.url), "utf8");

assert.match(
styles,
/\.netcatty-mdx-content\s+pre\s*\{[^}]*border:\s*0;[^}]*background:\s*transparent;[^}]*padding:\s*0;/s,
/\.netcatty-mdx-editor\s+\[class\*="_codeMirrorWrapper_"\]\s*\{[^}]*border:\s*0\s*!important;[^}]*padding:\s*0\.2rem\s+0\s*!important;/s,
);
assert.match(
styles,
/\.netcatty-note-code-copy\s*\{[^}]*border:\s*0\s*!important;[^}]*background:\s*transparent\s*!important;[^}]*box-shadow:\s*none\s*!important;/s,
/\.netcatty-mdx-editor\s+\.cm-editor\s*\{[^}]*border:\s*0\s*!important;/s,
);
assert.match(
styles,
Expand Down Expand Up @@ -569,37 +577,37 @@ test("note code block frame is borderless and language picker is compact", () =>
);
assert.match(
styles,
/\.netcatty-mdx-editor:not\(\.netcatty-mdx-editor--preview\)\s+\[class\*="_codeMirrorWrapper_"\]\s*\{[^}]*gap:\s*0;[^}]*margin:\s*0\.25rem\s+0\s+0\.55rem;/s,
/\.netcatty-mdx-editor:not\(\.netcatty-mdx-editor--preview\)\s+\[class\*="_codeMirrorWrapper_"\]\s*\{[^}]*gap:\s*0;[^}]*margin:\s*0\.3rem\s+0\s+0\.65rem;/s,
);
assert.match(
styles,
/\.netcatty-mdx-editor:not\(\.netcatty-mdx-editor--preview\)\s+\[class\*="_codeMirrorWrapper_"\]\s+\.cm-content\s*\{[^}]*padding:\s*0\s*!important;/s,
);
assert.match(
styles,
/\.netcatty-mdx-editor\s+\.cm-gutters\s*\{[^}]*background:\s*transparent\s*!important;[^}]*padding:\s*0\s*!important;/s,
/\.netcatty-mdx-editor\s+\.cm-gutters\s*\{[^}]*padding:\s*0\s*!important;/s,
);
assert.match(
styles,
/\.netcatty-mdx-editor--preview\s+\[class\*="_codeMirrorToolbar_"\]\s*\{[^}]*display:\s*none\s*!important;/s,
);
});

test("note formulas render without framed surfaces", () => {
test("note formulas render with framed surfaces", () => {
const styles = readFileSync(new URL("../../index.css", import.meta.url), "utf8");

assert.doesNotMatch(styles, /data-language="math"/);
assert.match(
styles,
/\.netcatty-math-formula-preview\s*\{[^}]*background:\s*transparent;[^}]*border:\s*0;/s,
/\.netcatty-math-formula-preview\s*\{[^}]*background:\s*hsl\(var\(--card\)\s*\/\s*0\.85\);[^}]*border:\s*1px solid hsl\(var\(--border\)\s*\/\s*0\.6\);/s,
);
assert.match(
styles,
/\.netcatty-math-reading-mode\s*\{[^}]*background:\s*transparent\s*!important;[^}]*border:\s*none\s*!important;[^}]*padding:\s*0\s*!important;/s,
/\.netcatty-math-reading-mode\s*\{[^}]*background:\s*transparent\s*!important;[^}]*border:\s*none\s*!important;/s,
);
assert.match(
styles,
/\.netcatty-math-reading-mode\s+\.netcatty-math-formula-preview\s*\{[^}]*background:\s*transparent;/s,
/\.netcatty-math-reading-mode\s+\.netcatty-math-formula-preview\s*\{[^}]*background:\s*hsl\(var\(--secondary\)\s*\/\s*0\.25\);/s,
);
assert.match(styles, /\.netcatty-math-formula-preview\s*\{[^}]*justify-content:\s*safe center;[^}]*overflow-x:\s*auto;/s);
assert.match(
Expand Down
59 changes: 52 additions & 7 deletions components/notes/InlineMarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
thematicBreakPlugin,
} from "@mdxeditor/editor";
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tooltips } from "@codemirror/view";
import { tags } from "@lezer/highlight";
import { ExternalLink } from "lucide-react";
import {
Expand Down Expand Up @@ -294,7 +295,6 @@ const NOTE_CODE_BLOCK_LANGUAGES = {
sh: "Shell",
shell: "Shell",
sql: "SQL",
tex: "TeX",
toml: "TOML",
Comment on lines 297 to 298

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the existing TeX language alias registered

Removing tex from NOTE_CODE_BLOCK_LANGUAGES means existing notes containing fenced ```tex blocks no longer match the language map passed to codeMirrorPlugin, so they lose the CodeMirror editor/language controls even though isNoteMathLanguageLabel still explicitly recognizes tex as a supported formula format. Preserve the alias for backward compatibility with already-saved notes.

Useful? React with 👍 / 👎.

ts: "TypeScript",
tsx: "TypeScript (React)",
Expand Down Expand Up @@ -322,7 +322,12 @@ const noteCodeHighlightStyle = HighlightStyle.define([
{ tag: tags.invalid, class: "netcatty-code-token-invalid" },
]);

const NOTE_CODE_MIRROR_EXTENSIONS = [syntaxHighlighting(noteCodeHighlightStyle)];
const NOTE_CODE_MIRROR_EXTENSIONS = [
syntaxHighlighting(noteCodeHighlightStyle),
tooltips({
parent: typeof document !== "undefined" ? document.body : undefined,
}),
];

type RectLike = Pick<DOMRect, "bottom" | "height" | "left" | "top" | "width">;

Expand Down Expand Up @@ -799,14 +804,35 @@ export const annotateNoteCodeBlockDeleteButtons = (container: HTMLElement): void
export const isNoteMathLanguageLabel = (value: string): boolean => {
const normalized = value.toLowerCase().trim();
if (!normalized) return false;
return normalized === "latex"
return (
normalized === "latex"
|| normalized === "tex"
|| /(?:^|\s)language-(?:latex|tex)(?:\s|$)/.test(normalized);
|| normalized === "math"
|| normalized === "katex"
|| normalized === "formula"
|| normalized === "公式"
|| normalized === "math (latex)"
|| /(?:^|\s)language-(?:latex|tex|math|katex|formula)(?:\s|$)/.test(normalized)
|| /^(?:math|latex|tex|katex|formula|公式)(?:\s|\(|$)/.test(normalized)
);
};

export const shouldRenderNoteMathFormula = (
languageLabel: string,
): boolean => isNoteMathLanguageLabel(languageLabel);
content?: string,
): boolean => {
if (isNoteMathLanguageLabel(languageLabel)) return true;
if (content) {
const trimmed = content.trim();
if (trimmed.startsWith("$$") && trimmed.endsWith("$$") && trimmed.length >= 4) {
return true;
}
if (trimmed.startsWith("\\[") && trimmed.endsWith("\\]") && trimmed.length >= 4) {
return true;
}
}
return false;
};

export const annotateMathFormulaBlocks = (container: HTMLElement, editorMode: string): void => {
container.querySelectorAll('[class*="_codeMirrorWrapper_"], pre').forEach((wrapper) => {
Expand All @@ -825,7 +851,7 @@ export const annotateMathFormulaBlocks = (container: HTMLElement, editorMode: st

const text = getCodeMirrorBlockText(wrapper).trim();

const isMathBlock = shouldRenderNoteMathFormula(lang);
const isMathBlock = shouldRenderNoteMathFormula(lang, text);
if (!isMathBlock) {
const existingPreview = wrapper.querySelector(".netcatty-math-formula-preview");
if (existingPreview) existingPreview.remove();
Expand All @@ -850,7 +876,11 @@ export const annotateMathFormulaBlocks = (container: HTMLElement, editorMode: st

if (preview.dataset.formulaSource !== formulaSource) {
preview.dataset.formulaSource = formulaSource;
preview.innerHTML = renderNoteMathFormula(formulaSource);
try {
preview.innerHTML = renderNoteMathFormula(formulaSource);
} catch {
preview.textContent = formulaSource;
}
}

if (editorMode === "preview") {
Expand All @@ -860,6 +890,21 @@ export const annotateMathFormulaBlocks = (container: HTMLElement, editorMode: st
}
});

if (editorMode === "preview") {
container.querySelectorAll("p").forEach((p) => {
const pText = p.textContent?.trim() || "";
if (pText.startsWith("$$") && pText.endsWith("$$") && pText.length >= 4) {
if (p.querySelector(".katex, math")) return;
const formula = pText.slice(2, -2).trim();
try {
p.innerHTML = renderNoteMathFormula(formula);
p.classList.add("netcatty-math-block-p");
} catch {
// ignore
}
}
});
}
};

const deleteLexicalTextRange = (range: Range, onUpdate: () => void): boolean => {
Expand Down
Loading
Loading