mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 18:52:39 +00:00
fix(markdown): preserve spacing between HTML elements
This commit is contained in:
@@ -9,6 +9,25 @@ const HTML = new RegExp(
|
||||
String.raw`(?<!\\)(?:<!--[\s\S]*?-->|<code\b[^>]*>[\s\S]*?<\/code\s*>|${TAG})`,
|
||||
"gi",
|
||||
);
|
||||
const BLOCK_TAGS = new Set([
|
||||
"div",
|
||||
"p",
|
||||
"table",
|
||||
"tr",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"section",
|
||||
"article",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"blockquote",
|
||||
"pre",
|
||||
]);
|
||||
const EMPHASIS: Readonly<Record<string, string>> = { b: "**", strong: "**", i: "*", em: "*" };
|
||||
|
||||
/** Convert comment HTML to Markdown while preserving the original spelling of code. */
|
||||
@@ -48,11 +67,10 @@ export function normalizeMarkdownHtml(source: string): string {
|
||||
const closing = raw.startsWith("</");
|
||||
const name = /^<\/?([\w-]+)/.exec(raw)?.[1].toLowerCase() ?? "";
|
||||
if (EMPHASIS[name]) return EMPHASIS[name];
|
||||
if (name === "br") return closing ? "" : " \n";
|
||||
if (name === "summary") return closing ? "**\n\n" : "**";
|
||||
if (name === "details")
|
||||
return (closing ? offset + raw.length === source.length : offset === 0) ? "" : "\n\n";
|
||||
if (name === "a") return markdownAnchor(raw, closing, links);
|
||||
const boundary = layoutBoundary(name, source, raw, offset, closing);
|
||||
if (boundary !== null) return boundary;
|
||||
if (name === "a")
|
||||
return markdownAnchor(raw, closing, links) + inlineBoundary(source, raw, offset, closing);
|
||||
if (name === "img" && !closing) {
|
||||
const { src, alt = "" } = attributes(raw);
|
||||
return src ? `![${alt.replace(/[[\]\\]/g, "\\$&")}](<${markdownUrl(src)}>)` : alt;
|
||||
@@ -111,3 +129,41 @@ function markdownAnchor(raw: string, closing: boolean, links: (string | undefine
|
||||
links.push(href);
|
||||
return href === undefined ? "" : "[";
|
||||
}
|
||||
|
||||
function inlineBoundary(source: string, raw: string, offset: number, closing: boolean): string {
|
||||
if (!closing) return "";
|
||||
return /^(?:<[A-Za-z]|[A-Za-z0-9])/.test(source.slice(offset + raw.length)) ? " " : "";
|
||||
}
|
||||
|
||||
function blockBoundary(source: string, raw: string, offset: number, closing: boolean): string {
|
||||
if (closing) {
|
||||
const following = source.slice(offset + raw.length);
|
||||
if (following.trim() === "" || following.startsWith("\n\n")) return "";
|
||||
const nextClosingTag = /^<\/([\w-]+)/.exec(following.trimStart());
|
||||
if (nextClosingTag && BLOCK_TAGS.has(nextClosingTag[1].toLowerCase())) return "";
|
||||
} else {
|
||||
const preceding = source.slice(0, offset).trimEnd();
|
||||
if (!preceding) return "";
|
||||
const previousTag = /<\/?([\w-]+)[^>]*>$/.exec(preceding);
|
||||
if (previousTag && BLOCK_TAGS.has(previousTag[1].toLowerCase())) return "";
|
||||
}
|
||||
return "\n\n";
|
||||
}
|
||||
|
||||
function layoutBoundary(
|
||||
name: string,
|
||||
source: string,
|
||||
raw: string,
|
||||
offset: number,
|
||||
closing: boolean,
|
||||
): string | null {
|
||||
if (name === "br") return closing ? "" : " \n";
|
||||
if (name === "summary") return closing ? "**\n\n" : "**";
|
||||
if (name === "details")
|
||||
return (closing ? offset + raw.length === source.length : offset === 0) ? "" : "\n\n";
|
||||
if (BLOCK_TAGS.has(name)) return blockBoundary(source, raw, offset, closing);
|
||||
if ((name === "td" || name === "th") && closing)
|
||||
return /^<t[dh]\b/i.test(source.slice(offset + raw.length)) ? " " : "";
|
||||
if (name === "span") return inlineBoundary(source, raw, offset, closing);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -311,3 +311,20 @@ it("uses the first repeated image reference definition", () => {
|
||||
"\n\n[image]: first.png\n[image]: second.png",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps Greptile header links and block text separated", () => {
|
||||
const source =
|
||||
'<h2><a href="https://app.greptile.com/api/retrigger?id=81261069"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/RetriggerDark.svg?v=2"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"><img alt="Retrigger" src="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2" align="right"></picture></a><a href="https://app.greptile.com/paseo/github/getpaseo/paseo/-/pull/6973"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptileDark.svg?v=2"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptile.svg?v=2"><img alt="View in Greptile" src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptile.svg?v=2" align="right"></picture></a>Confidence Score: 2/5</h2>';
|
||||
const rendered = prepareMarkdownSource(source, undefined, { supportsSvg: false }).render();
|
||||
expect(rendered).toContain(
|
||||
"[Retrigger](<https://app.greptile.com/api/retrigger?id=81261069>) [View in Greptile]",
|
||||
);
|
||||
expect(rendered).toContain(") Confidence Score: 2/5");
|
||||
});
|
||||
|
||||
it("keeps paragraph, heading and table-row boundaries when stripping HTML", () => {
|
||||
const source =
|
||||
"<div>First</div><p>Second<br>line</p><h3>Heading</h3><table><tr><td>A</td><td>B</td></tr><tr><td>C</td><td>D</td></tr></table>";
|
||||
const rendered = prepareMarkdownSource(source).render();
|
||||
expect(rendered).toBe("First\n\nSecond \nline\n\nHeading\n\nA B\n\nC D");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user