mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 01:42:59 +00:00
fix(telegram): preserve fenced code languages (#85209)
Co-authored-by: wuyangfan <yangfan.wu@succaiss.com>
This commit is contained in:
@@ -96,9 +96,9 @@ describe("markdownToTelegramHtml", () => {
|
||||
expect(res.match(/<blockquote>/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("renders fenced code blocks", () => {
|
||||
const res = markdownToTelegramHtml("```js\nconst x = 1;\n```");
|
||||
expect(res).toBe("<pre><code>const x = 1;\n</code></pre>");
|
||||
it("renders fenced code block languages for Telegram native copy buttons", () => {
|
||||
const res = markdownToTelegramHtml('```bash\necho "hello"\n```');
|
||||
expect(res).toBe('<pre><code class="language-bash">echo "hello"\n</code></pre>');
|
||||
});
|
||||
|
||||
it("properly nests overlapping bold and autolink (#4071)", () => {
|
||||
|
||||
@@ -60,6 +60,13 @@ function buildTelegramLink(link: MarkdownLinkSpan, text: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildTelegramCodeBlockOpen(span: { language?: string }): string {
|
||||
if (!span.language) {
|
||||
return "<pre><code>";
|
||||
}
|
||||
return `<pre><code class="language-${escapeHtmlAttr(span.language)}">`;
|
||||
}
|
||||
|
||||
function renderTelegramHtml(ir: MarkdownIR): string {
|
||||
return renderMarkdownWithMarkers(ir, {
|
||||
styleMarkers: {
|
||||
@@ -67,7 +74,7 @@ function renderTelegramHtml(ir: MarkdownIR): string {
|
||||
italic: { open: "<i>", close: "</i>" },
|
||||
strikethrough: { open: "<s>", close: "</s>" },
|
||||
code: { open: "<code>", close: "</code>" },
|
||||
code_block: { open: "<pre><code>", close: "</code></pre>" },
|
||||
code_block: { open: buildTelegramCodeBlockOpen, close: "</code></pre>" },
|
||||
spoiler: { open: "<tg-spoiler>", close: "</tg-spoiler>" },
|
||||
blockquote: { open: "<blockquote>", close: "</blockquote>" },
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ type RenderEnv = {
|
||||
type MarkdownToken = {
|
||||
type: string;
|
||||
content?: string;
|
||||
info?: string;
|
||||
children?: MarkdownToken[];
|
||||
attrs?: [string, string][];
|
||||
attrGet?: (name: string) => string | null;
|
||||
@@ -40,6 +41,7 @@ export type MarkdownStyleSpan = {
|
||||
start: number;
|
||||
end: number;
|
||||
style: MarkdownStyle;
|
||||
language?: string;
|
||||
};
|
||||
|
||||
export type MarkdownLinkSpan = {
|
||||
@@ -54,6 +56,18 @@ export type MarkdownIR = {
|
||||
links: MarkdownLinkSpan[];
|
||||
};
|
||||
|
||||
function createStyleSpan(params: MarkdownStyleSpan): MarkdownStyleSpan {
|
||||
const span: MarkdownStyleSpan = {
|
||||
start: params.start,
|
||||
end: params.end,
|
||||
style: params.style,
|
||||
};
|
||||
if (params.language) {
|
||||
span.language = params.language;
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
export type MarkdownTableData = {
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
@@ -325,7 +339,12 @@ function renderInlineCode(state: RenderState, content: string) {
|
||||
target.styles.push({ start, end: start + content.length, style: "code" });
|
||||
}
|
||||
|
||||
function renderCodeBlock(state: RenderState, content: string) {
|
||||
function resolveFenceLanguage(info: string | undefined): string | undefined {
|
||||
const language = info?.trim().split(/\s+/, 1)[0]?.trim();
|
||||
return language || undefined;
|
||||
}
|
||||
|
||||
function renderCodeBlock(state: RenderState, content: string, info?: string) {
|
||||
let code = content ?? "";
|
||||
if (!code.endsWith("\n")) {
|
||||
code = `${code}\n`;
|
||||
@@ -333,7 +352,14 @@ function renderCodeBlock(state: RenderState, content: string) {
|
||||
const target = resolveRenderTarget(state);
|
||||
const start = target.text.length;
|
||||
target.text += code;
|
||||
target.styles.push({ start, end: start + code.length, style: "code_block" });
|
||||
target.styles.push(
|
||||
createStyleSpan({
|
||||
start,
|
||||
end: start + code.length,
|
||||
style: "code_block",
|
||||
language: resolveFenceLanguage(info),
|
||||
}),
|
||||
);
|
||||
if (state.env.listStack.length === 0) {
|
||||
target.text += "\n";
|
||||
}
|
||||
@@ -732,7 +758,7 @@ function renderTokens(tokens: MarkdownToken[], state: RenderState): void {
|
||||
break;
|
||||
case "code_block":
|
||||
case "fence":
|
||||
renderCodeBlock(state, token.content ?? "");
|
||||
renderCodeBlock(state, token.content ?? "", token.info);
|
||||
break;
|
||||
case "html_block":
|
||||
case "html_inline":
|
||||
@@ -834,7 +860,7 @@ function clampStyleSpans(spans: MarkdownStyleSpan[], maxLength: number): Markdow
|
||||
const start = Math.max(0, Math.min(span.start, maxLength));
|
||||
const end = Math.max(start, Math.min(span.end, maxLength));
|
||||
if (end > start) {
|
||||
clamped.push({ start, end, style: span.style });
|
||||
clamped.push(createStyleSpan({ start, end, style: span.style, language: span.language }));
|
||||
}
|
||||
}
|
||||
return clamped;
|
||||
@@ -869,6 +895,7 @@ function mergeStyleSpans(spans: MarkdownStyleSpan[]): MarkdownStyleSpan[] {
|
||||
if (
|
||||
prev &&
|
||||
prev.style === span.style &&
|
||||
prev.language === span.language &&
|
||||
// Blockquotes are container blocks. Adjacent blockquote spans should not merge or
|
||||
// consecutive blockquotes can "style bleed" across the paragraph boundary.
|
||||
(span.start < prev.end || (span.start === prev.end && span.style !== "blockquote"))
|
||||
@@ -908,11 +935,14 @@ function sliceStyleSpans(
|
||||
if (!bounds) {
|
||||
continue;
|
||||
}
|
||||
sliced.push({
|
||||
start: bounds.start - start,
|
||||
end: bounds.end - start,
|
||||
style: span.style,
|
||||
});
|
||||
sliced.push(
|
||||
createStyleSpan({
|
||||
start: bounds.start - start,
|
||||
end: bounds.end - start,
|
||||
style: span.style,
|
||||
language: span.language,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return mergeStyleSpans(sliced);
|
||||
}
|
||||
|
||||
@@ -215,7 +215,12 @@ function mergeAdjacentStyleSpans(styles: MarkdownStyleSpan[]): MarkdownStyleSpan
|
||||
const merged: MarkdownStyleSpan[] = [];
|
||||
for (const span of styles) {
|
||||
const last = merged.at(-1);
|
||||
if (last && last.style === span.style && span.start <= last.end) {
|
||||
if (
|
||||
last &&
|
||||
last.style === span.style &&
|
||||
last.language === span.language &&
|
||||
span.start <= last.end
|
||||
) {
|
||||
last.end = Math.max(last.end, span.end);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MarkdownIR, MarkdownLinkSpan, MarkdownStyle, MarkdownStyleSpan } from "./ir.js";
|
||||
|
||||
export type RenderStyleMarker = {
|
||||
open: string;
|
||||
open: string | ((span: MarkdownStyleSpan) => string);
|
||||
close: string;
|
||||
};
|
||||
|
||||
@@ -153,7 +153,7 @@ export function renderMarkdownWithMarkers(ir: MarkdownIR, options: RenderOptions
|
||||
}
|
||||
openingItems.push({
|
||||
end: span.end,
|
||||
open: marker.open,
|
||||
open: typeof marker.open === "function" ? marker.open(span) : marker.open,
|
||||
close: marker.close,
|
||||
kind: "style",
|
||||
style: span.style,
|
||||
|
||||
Reference in New Issue
Block a user