Files
multica/packages/views/editor/utils/preprocess.ts
d09019cd95 MUL-5839: preserve media in /issue descriptions (#6536)
* fix(channel): preserve images in issue descriptions

* fix(views): hide channel-media provenance in board card previews

descriptionPreview strips image Markdown but not HTML comments, so a
`/issue` description whose media was materialized rendered its provenance
marker as the card's visible preview text. cardProperties.description
defaults to true, so this showed on the default board view for every issue
created from a channel message carrying an image.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-07 15:35:59 +08:00

60 lines
2.9 KiB
TypeScript

import {
preprocessLinks,
preprocessMentionShortcodes,
preprocessFileCards,
preprocessIssueIdentifiers,
} from "@multica/ui/markdown";
import { stripChannelMediaMarkers } from "@multica/core/types";
/**
* Preprocess a markdown string before loading into Tiptap via contentType: 'markdown'.
*
* This is the ONLY transform applied before @tiptap/markdown parses the content.
* It does NOT convert to HTML — that was the old markdownToHtml.ts pipeline which
* was deleted in the April 2026 refactor.
*
* String→string transforms on raw Markdown:
* 1. Legacy mention shortcodes [@ id="..." label="..."] → [@Label](mention://member/id)
* (old serialization format in database, migrated on read)
* 2. (readonly only) Bare issue identifiers MUL-123 → [MUL-123](mention://issue/MUL-123)
* 3. Raw URLs → markdown links via linkify-it (so they render as clickable Link nodes)
* 4. File card syntax (new !file[name](url) + legacy [name](cdnUrl)) → HTML div for
* fileCard node parsing
*
* Shared by the Tiptap editor and the read-only react-markdown renderer so both
* linkify identically. `autolinkIssueIdentifiers` is the one deliberate
* asymmetry: it is OPT-IN and MUST stay off for the editable Tiptap path, since
* rewriting a bare identifier there would create a mention node whose id is the
* identifier string (not a real UUID) and corrupt the saved markdown. Only the
* readonly renderer (which resolves the identifier to a UUID at render time)
* passes it.
*
* `cdnDomain` is an explicit parameter rather than an imperative
* `configStore.getState()` read inside this function. The CDN config arrives
* asynchronously after auth, so a one-shot read made this transform silently
* time-dependent: content rendered before the config landed kept its legacy CDN
* links as plain anchors forever, because nothing re-ran the transform. Passing
* the value in lets a reactive caller (RichContent, which subscribes to the
* store) put it in its memo dependencies, while a one-shot caller (the Tiptap
* editor, which preprocesses once at load) can still read the store itself.
*/
export function preprocessMarkdown(
markdown: string,
opts: { cdnDomain: string; autolinkIssueIdentifiers?: boolean },
): string {
if (!markdown) return "";
const { cdnDomain } = opts;
// Channel-media provenance is persisted for optimistic merge safety, not
// authored content. Remove only this namespaced comment before either the
// editable Tiptap parser or readonly renderer sees it; the ContentEditor
// separately retains the raw controlled value as its server merge base.
const visibleMarkdown = stripChannelMediaMarkers(markdown);
const step1 = preprocessMentionShortcodes(visibleMarkdown);
const step2 = opts?.autolinkIssueIdentifiers
? preprocessIssueIdentifiers(step1)
: step1;
const step3 = preprocessLinks(step2);
const step4 = preprocessFileCards(step3, cdnDomain);
return step4;
}