mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-25 20:15:37 +02:00
* feat(editor): support text highlight (==text==) in description & comments Adds a single-color (yellow) text highlight mark to the shared rich-text editor, round-tripped through stored Markdown as ==text==. - HighlightExtension: @tiptap/extension-highlight + @tiptap/markdown hooks (markdownTokenizer/parseMarkdown/renderMarkdown) so ==text== <-> <mark> round-trips; inner inline formatting preserved via inlineTokens. - Bubble menu: highlight toggle button (Mod-Shift-H), i18n in 4 locales. - Read-only renderer: highlightToHtml lowers ==text== -> <mark> (skips code and math); rehype-sanitize schema whitelists <mark>. Nested Markdown inside a highlight still parses via the existing rehype-raw step. - prose.css: single yellow <mark> style, legible in light/dark. Pinned @tiptap/extension-highlight to exact 3.22.1 to match @tiptap/core (>=3.23 expects a getStyleProperty export core 3.22.1 doesn't have). Web/desktop only. Mobile (native md4c, no == syntax, no custom renderers) is tracked as a follow-up. MUL-2934. Tests: editor round-trip (cross-process serialization protocol), readonly <mark> rendering + sanitize, and the ==->mark transform incl. code-skip. Co-authored-by: multica-agent <github@multica.ai> * fix(editor): align highlight boundary rules across editor & readonly Addresses two boundary bugs from review (PR #3661): 1. A == inside inline code/math could close a highlight when the opening == was outside the literal span (e.g. ==a `b==c` d== wrongly became <mark>a `b</mark>c` d==). Both the editor tokenizer's lazy regex and the readonly transform only guarded the opening fence, not the closing one. 2. The readonly transform matched across blank lines (==a\n\nb==) while the editor lexes those as two literal paragraphs — a storage↔editor↔readonly mismatch. Fix: extract one shared matcher (utils/highlight-match.ts) used by BOTH the editor tokenizer and the readonly lowering, so the rules can't drift. It skips fences that fall inside code/math literal ranges (open or close) and caps the inner span at the first blank line. Tests: shared-matcher unit tests + both repros covered on the editor (round-trip/HTML) and readonly (transform + rendered DOM) sides. Co-authored-by: multica-agent <github@multica.ai> * fix(editor): handle CRLF in highlight blank-line boundary BLANK_LINE_RE only matched LF, so a CRLF blank line (==a\r\n\r\nb==) was not recognized as a block boundary and got highlighted. Widen to \r?\n[ \t]*\r?\n. Tests: CRLF blank-line (no highlight) + CRLF soft-break (still highlights) on the matcher, readonly transform, and editor sides. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { highlightToHtml } from "./highlight-markdown";
|
|
|
|
describe("highlightToHtml", () => {
|
|
it("lowers a basic highlight to <mark>", () => {
|
|
expect(highlightToHtml("a ==hi== b")).toBe("a <mark>hi</mark> b");
|
|
});
|
|
|
|
it("keeps inner markdown intact for nested formatting", () => {
|
|
expect(highlightToHtml("==**bold**==")).toBe("<mark>**bold**</mark>");
|
|
});
|
|
|
|
it("handles multiple highlights on one line", () => {
|
|
expect(highlightToHtml("==a== and ==b==")).toBe(
|
|
"<mark>a</mark> and <mark>b</mark>",
|
|
);
|
|
});
|
|
|
|
it("requires non-space directly inside the fences", () => {
|
|
expect(highlightToHtml("== spaced ==")).toBe("== spaced ==");
|
|
});
|
|
|
|
it("does not match empty fences", () => {
|
|
expect(highlightToHtml("====")).toBe("====");
|
|
});
|
|
|
|
it("is a no-op when there is no ==", () => {
|
|
const md = "plain **bold** _italic_ text";
|
|
expect(highlightToHtml(md)).toBe(md);
|
|
});
|
|
|
|
it("ignores == inside inline code", () => {
|
|
expect(highlightToHtml("`a ==b== c`")).toBe("`a ==b== c`");
|
|
});
|
|
|
|
it("ignores == inside fenced code blocks", () => {
|
|
const md = "```\nx ==y== z\n```";
|
|
expect(highlightToHtml(md)).toBe(md);
|
|
});
|
|
|
|
it("ignores == inside inline math", () => {
|
|
expect(highlightToHtml("$a ==b== c$")).toBe("$a ==b== c$");
|
|
});
|
|
|
|
it("highlights outside code while leaving code untouched", () => {
|
|
expect(highlightToHtml("==hi== `x ==y==`")).toBe(
|
|
"<mark>hi</mark> `x ==y==`",
|
|
);
|
|
});
|
|
|
|
it("does not treat a == b (comparison) as a highlight", () => {
|
|
expect(highlightToHtml("if a == b then")).toBe("if a == b then");
|
|
});
|
|
|
|
// Boundary regressions (Emacs review, PR #3661).
|
|
|
|
it("does not let a == inside inline code close the highlight", () => {
|
|
// The inner `==` lives in inline code; the highlight must wrap the whole
|
|
// span, not stop at the code's `==`.
|
|
expect(highlightToHtml("==a `b==c` d==")).toBe(
|
|
"<mark>a `b==c` d</mark>",
|
|
);
|
|
});
|
|
|
|
it("does not let a == inside inline math close the highlight", () => {
|
|
expect(highlightToHtml("==a $b==c$ d==")).toBe(
|
|
"<mark>a $b==c$ d</mark>",
|
|
);
|
|
});
|
|
|
|
it("does not highlight across a blank line / block boundary", () => {
|
|
expect(highlightToHtml("==a\n\nb==")).toBe("==a\n\nb==");
|
|
});
|
|
|
|
it("still highlights across a soft line break within a block", () => {
|
|
expect(highlightToHtml("==a\nb==")).toBe("<mark>a\nb</mark>");
|
|
});
|
|
|
|
it("does not highlight across a CRLF blank line", () => {
|
|
expect(highlightToHtml("==a\r\n\r\nb==")).toBe("==a\r\n\r\nb==");
|
|
});
|
|
|
|
it("still highlights across a CRLF soft line break", () => {
|
|
expect(highlightToHtml("==a\r\nb==")).toBe("<mark>a\r\nb</mark>");
|
|
});
|
|
});
|