fix(telegram): properly nest overlapping HTML tags (#4071)
Unify style and link closing in render.ts to use LIFO order across both element types, fixing cases where bold/italic spans containing autolinks produced invalid HTML like <b><a></b></a>.
This commit is contained in:
parent
fa9ec6e854
commit
b05d57964b
@ -87,40 +87,36 @@ export function renderMarkdownWithMarkers(ir: MarkdownIR, options: RenderOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
const points = [...boundaries].sort((a, b) => a - b);
|
const points = [...boundaries].sort((a, b) => a - b);
|
||||||
const stack: MarkdownStyleSpan[] = [];
|
// Unified stack for both styles and links, tracking close string and end position
|
||||||
|
const stack: { close: string; end: number }[] = [];
|
||||||
let out = "";
|
let out = "";
|
||||||
|
|
||||||
for (let i = 0; i < points.length; i += 1) {
|
for (let i = 0; i < points.length; i += 1) {
|
||||||
const pos = points[i];
|
const pos = points[i];
|
||||||
|
|
||||||
|
// Close ALL elements (styles and links) in LIFO order at this position
|
||||||
while (stack.length && stack[stack.length - 1]?.end === pos) {
|
while (stack.length && stack[stack.length - 1]?.end === pos) {
|
||||||
const span = stack.pop();
|
const item = stack.pop();
|
||||||
if (!span) break;
|
if (item) out += item.close;
|
||||||
const marker = styleMarkers[span.style];
|
|
||||||
if (marker) out += marker.close;
|
|
||||||
}
|
|
||||||
|
|
||||||
const closingLinks = linkEnds.get(pos);
|
|
||||||
if (closingLinks && closingLinks.length > 0) {
|
|
||||||
for (const link of closingLinks) {
|
|
||||||
out += link.close;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open links first (so they close after styles that start at the same position)
|
||||||
const openingLinks = linkStarts.get(pos);
|
const openingLinks = linkStarts.get(pos);
|
||||||
if (openingLinks && openingLinks.length > 0) {
|
if (openingLinks && openingLinks.length > 0) {
|
||||||
for (const link of openingLinks) {
|
for (const link of openingLinks) {
|
||||||
out += link.open;
|
out += link.open;
|
||||||
|
stack.push({ close: link.close, end: link.end });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open styles second (so they close before links that start at the same position)
|
||||||
const openingStyles = startsAt.get(pos);
|
const openingStyles = startsAt.get(pos);
|
||||||
if (openingStyles) {
|
if (openingStyles) {
|
||||||
for (const span of openingStyles) {
|
for (const span of openingStyles) {
|
||||||
const marker = styleMarkers[span.style];
|
const marker = styleMarkers[span.style];
|
||||||
if (!marker) continue;
|
if (!marker) continue;
|
||||||
stack.push(span);
|
|
||||||
out += marker.open;
|
out += marker.open;
|
||||||
|
stack.push({ close: marker.close, end: span.end });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,4 +47,14 @@ describe("markdownToTelegramHtml", () => {
|
|||||||
const res = markdownToTelegramHtml("```js\nconst x = 1;\n```");
|
const res = markdownToTelegramHtml("```js\nconst x = 1;\n```");
|
||||||
expect(res).toBe("<pre><code>const x = 1;\n</code></pre>");
|
expect(res).toBe("<pre><code>const x = 1;\n</code></pre>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("properly nests overlapping bold and autolink (#4071)", () => {
|
||||||
|
const res = markdownToTelegramHtml("**start https://example.com** end");
|
||||||
|
expect(res).toMatch(/<b>start <a href="https:\/\/example\.com">https:\/\/example\.com<\/a><\/b> end/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("properly nests link inside bold", () => {
|
||||||
|
const res = markdownToTelegramHtml("**bold [link](https://example.com) text**");
|
||||||
|
expect(res).toBe('<b>bold <a href="https://example.com">link</a> text</b>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user