Files
openclaw/extensions/imessage/src/markdown-format.ts
Peter Steinberger aae10cade5 refactor(markdown): unify format profile declarations (#113254)
* refactor(markdown): unify format profile declarations

* test(markdown): avoid suppression inventory drift
2026-07-24 00:37:59 -07:00

190 lines
5.6 KiB
TypeScript

import {
FormatCapabilityProfile,
markdownToIR,
renderMarkdownWithAttributedRanges,
} from "openclaw/plugin-sdk/text-chunking";
type IMessageFormatStyle = "bold" | "italic" | "underline" | "strikethrough";
type IMessageFormatRange = {
start: number;
length: number;
styles: IMessageFormatStyle[];
};
const IMESSAGE_FORMAT_PROFILE = FormatCapabilityProfile.define({
mechanism: "ranges",
constructs: {
spoiler: "strip",
codeInline: "fallback",
codeBlock: "fallback",
codeLanguage: "strip",
linkLabel: "fallback",
heading: "fallback",
bulletList: "fallback",
orderedList: "fallback",
taskList: "fallback",
table: "fallback",
blockquote: "fallback",
image: "fallback",
mention: "strip",
},
chunk: { limit: 4_000, unit: "utf16" },
});
const IMESSAGE_CODE_PROFILE = FormatCapabilityProfile.define({
...IMESSAGE_FORMAT_PROFILE,
constructs: { ...IMESSAGE_FORMAT_PROFILE.constructs, codeInline: "native" },
});
const IMESSAGE_STYLE_MAP = {
bold: "bold",
italic: "italic",
underline: "underline",
strikethrough: "strikethrough",
} as const;
function codeDelimiter(content: string): string {
const longestRun = Math.max(0, ...[...content.matchAll(/`+/gu)].map((match) => match[0].length));
return "`".repeat(longestRun + 1);
}
type TextEdit = { start: number; end: number; text: string };
type DunderProtection = { token: string; identifier: string };
function protectDunderIdentifiers(input: string): {
markdown: string;
protections: DunderProtection[];
} {
const protections: DunderProtection[] = [];
let markdown = "";
let cursor = 0;
for (const match of input.matchAll(/__[\p{L}_][\p{L}\p{N}_]*__/gu)) {
const identifier = match[0];
const start = match.index ?? 0;
const end = start + identifier.length;
const before = input[start - 1];
const beforeBefore = input[start - 2];
const after = input[end];
const member = before === ".";
const call = after === "(";
const functionArgument = before === "(" && /[\p{L}\p{N}_]/u.test(beforeBefore ?? "");
const index = after === "[";
if (!member && !call && !functionArgument && !index) {
continue;
}
let token = `OCdunder${protections.length}token`;
while (input.includes(token)) {
token += "x";
}
markdown += input.slice(cursor, start) + token;
protections.push({ token, identifier });
cursor = end;
}
return { markdown: markdown + input.slice(cursor), protections };
}
function applyTextEdits(text: string, edits: TextEdit[]) {
const ordered = edits.toSorted((left, right) => left.start - right.start);
let rendered = "";
let cursor = 0;
for (const edit of ordered) {
rendered += text.slice(cursor, edit.start) + edit.text;
cursor = edit.end;
}
rendered += text.slice(cursor);
return {
text: rendered,
mapOffset: (offset: number) =>
offset +
ordered.reduce(
(delta, edit) =>
delta + (edit.end <= offset ? edit.text.length - edit.end + edit.start : 0),
0,
),
};
}
function restoreDunderIdentifiers(
text: string,
ranges: IMessageFormatRange[],
codeRanges: Array<{ start: number; length: number }>,
protections: DunderProtection[],
) {
const edits = protections.flatMap((protection) => {
const start = text.indexOf(protection.token);
return start === -1
? []
: [{ start, end: start + protection.token.length, text: protection.identifier }];
});
const edited = applyTextEdits(text, edits);
const mapRange = <T extends { start: number; length: number }>(range: T): T => ({
...range,
start: edited.mapOffset(range.start),
length: edited.mapOffset(range.start + range.length) - edited.mapOffset(range.start),
});
return {
text: edited.text,
ranges: ranges.map(mapRange),
codeRanges: codeRanges.map(mapRange),
};
}
function restoreCodeMarkers(
text: string,
ranges: Array<{ start: number; length: number; styles: IMessageFormatStyle[] }>,
codeRanges: Array<{ start: number; length: number }>,
): { text: string; ranges: IMessageFormatRange[] } {
const edits = codeRanges.map((range) => {
const end = range.start + range.length;
const content = text.slice(range.start, end);
const marker = codeDelimiter(content);
const padding = content.startsWith("`") || content.endsWith("`") ? " " : "";
return { start: range.start, end, text: `${marker}${padding}${content}${padding}${marker}` };
});
const edited = applyTextEdits(text, edits);
return {
text: edited.text,
ranges: ranges.map((range) => ({
...range,
start: edited.mapOffset(range.start),
length: edited.mapOffset(range.start + range.length) - edited.mapOffset(range.start),
})),
};
}
export function extractMarkdownFormatRuns(input: string): {
text: string;
ranges: IMessageFormatRange[];
} {
const protectedDunders = protectDunderIdentifiers(input);
const ir = markdownToIR(protectedDunders.markdown, {
autolink: false,
enableHtmlUnderline: true,
headingStyle: "rich",
linkify: false,
preserveSourceBlockSpacing: true,
});
const rendered = renderMarkdownWithAttributedRanges(
ir,
{ styleMap: IMESSAGE_STYLE_MAP },
IMESSAGE_FORMAT_PROFILE,
);
const code = renderMarkdownWithAttributedRanges(
ir,
{ styleMap: { code: "code" } },
IMESSAGE_CODE_PROFILE,
);
const dunders = restoreDunderIdentifiers(
rendered.text,
rendered.ranges.map(({ start, length, style }) => ({
start,
length,
styles: [style],
})),
code.ranges,
protectedDunders.protections,
);
return restoreCodeMarkers(dunders.text, dunders.ranges, dunders.codeRanges);
}