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>
57 lines
2.5 KiB
TypeScript
57 lines
2.5 KiB
TypeScript
import Highlight from "@tiptap/extension-highlight";
|
|
import { matchHighlightAt } from "../utils/highlight-match";
|
|
|
|
/**
|
|
* HighlightExtension — text highlight mark (`==text==` ⇄ <mark>).
|
|
*
|
|
* Builds on @tiptap/extension-highlight, which already supplies the `<mark>`
|
|
* parseHTML/renderHTML, the `==text==` input/paste rules, the
|
|
* setHighlight/toggleHighlight/unsetHighlight commands, and the Mod-Shift-H
|
|
* shortcut. On top of that we add @tiptap/markdown serialization so highlights
|
|
* round-trip through the stored Markdown as `==text==`:
|
|
*
|
|
* - renderMarkdown: highlight mark → `==…==`. @tiptap/markdown renders marks by
|
|
* calling renderMarkdown with a placeholder child and splitting the result
|
|
* into opening/closing fences, so wrapping the placeholder in `==` yields a
|
|
* `==` open and `==` close.
|
|
* - markdownTokenizer + parseMarkdown: `==text==` in stored Markdown → highlight
|
|
* mark. inlineTokens keeps inner inline formatting (e.g. `==**bold**==`).
|
|
*
|
|
* Single colour (yellow) for now — `multicolor` stays off. A future multicolour
|
|
* variant would need a syntax that can carry a colour (`==text==` cannot), so it
|
|
* is intentionally out of scope here (see MUL-2934).
|
|
*
|
|
* BOUNDARY RULES live in utils/highlight-match.ts and are shared with the
|
|
* read-only renderer (utils/highlight-markdown.ts) so the editor and the
|
|
* read-only view can never disagree on what counts as a highlight:
|
|
* - no whitespace directly inside the fences (`==x==` highlights, `== x ==` not)
|
|
* - non-empty content (`====` stays literal text)
|
|
* - neither fence may sit inside code/math (a `==` inside `` `code` `` / `$math$`
|
|
* is literal), so ``==a `b==c` d==`` highlights the whole span, not `a `b`
|
|
* - a highlight may not cross a blank line / block boundary
|
|
*/
|
|
|
|
export const HighlightExtension = Highlight.extend({
|
|
markdownTokenizer: {
|
|
name: "highlight",
|
|
level: "inline" as const,
|
|
start(src: string) {
|
|
return src.indexOf("==");
|
|
},
|
|
tokenize(src: string, _tokens: unknown, helpers: any) {
|
|
const match = matchHighlightAt(src, 0);
|
|
if (!match) return undefined;
|
|
return {
|
|
type: "highlight",
|
|
raw: src.slice(0, match.end),
|
|
tokens: helpers.inlineTokens(match.inner),
|
|
};
|
|
},
|
|
},
|
|
|
|
parseMarkdown: (token: any, helpers: any) =>
|
|
helpers.applyMark("highlight", helpers.parseInline(token.tokens)),
|
|
|
|
renderMarkdown: (_node: any, helpers: any) => `==${helpers.renderChildren()}==`,
|
|
}).configure({ multicolor: false });
|