feat(export/markdown): basic support for admonitions

This commit is contained in:
Elian Doran 2025-03-15 11:25:42 +02:00
parent b1e3ea4c80
commit 6d67e69e2f
No known key found for this signature in database
2 changed files with 142 additions and 0 deletions

View File

@ -103,4 +103,94 @@ describe("Markdown export", () => {
expect(markdownExportService.toMarkdown(html)).toBe(html);
});
it("exports admonitions properly", () => {
const html = trimIndentation`\
<p>
Before
</p>
<aside class="admonition note">
<p>
This is a note.
</p>
</aside>
<aside class="admonition tip">
<p>
This is a tip.
</p>
</aside>
<aside class="admonition important">
<p>
This is a very important information.
</p>
<figure class="table">
<table>
<tbody>
<tr>
<td>
1
</td>
<td>
2
</td>
</tr>
<tr>
<td>
3
</td>
<td>
4
</td>
</tr>
</tbody>
</table>
</figure>
</aside>
<aside class="admonition caution">
<p>
This is a caution.
</p>
</aside>
<aside class="admonition warning">
<h2>
Title goes here
</h2>
<p>
This is a warning.
</p>
</aside>
<p>
After
</p>
`;
const space = " "; // editor config trimming space.
const expected = trimIndentation`\
Before
> [!NOTE]
> This is a note.
> [!TIP]
> This is a tip.
> [!IMPORTANT]
> This is a very important information.
>${space}
> | | |
> | --- | --- |
> | 1 | 2 |
> | 3 | 4 |
> [!CAUTION]
> This is a caution.
> [!WARNING]
> ## Title goes here
>${space}
> This is a warning.
After`;
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
});

View File

@ -31,6 +31,7 @@ function toMarkdown(content: string) {
// Filter is heavily based on: https://github.com/mixmark-io/turndown/issues/274#issuecomment-458730974
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
instance.addRule("img", buildImageFilter());
instance.addRule("admonition", buildAdmonitionFilter());
instance.use(turndownPluginGfm.gfm);
instance.keep([ "kbd" ]);
}
@ -100,6 +101,57 @@ function buildImageFilter() {
return imageFilter;
}
function buildAdmonitionFilter() {
const admonitionTypeMappings: Record<string, string> = {
note: "NOTE",
tip: "TIP",
important: "IMPORTANT",
caution: "CAUTION",
warning: "WARNING"
};
const defaultAdmonitionType = admonitionTypeMappings.note;
function parseAdmonitionType(_node: Node) {
if (!("getAttribute" in _node)) {
return defaultAdmonitionType;
}
const node = _node as Element;
const classList = node.getAttribute("class")?.split(" ") ?? [];
for (const className of classList) {
if (className === "admonition") {
continue;
}
const mappedType = admonitionTypeMappings[className];
if (mappedType) {
return mappedType;
}
}
return defaultAdmonitionType;
}
const admonitionFilter: TurndownService.Rule = {
filter(node, options) {
return node.nodeName === "ASIDE" && node.classList.contains("admonition");
},
replacement(content, node) {
// Parse the admonition type.
const admonitionType = parseAdmonitionType(node);
content = content.replace(/^\n+|\n+$/g, '');
content = content.replace(/^/gm, '> ');
content = `> [!${admonitionType}]\n` + content;
return "\n\n" + content + "\n\n";
}
}
return admonitionFilter;
}
export default {
toMarkdown
};