Skip to content

Commit

Permalink
feat: custom tags and attributes (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
deer authored Jan 29, 2024
1 parent 3ac5949 commit b689881
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 50 deletions.
119 changes: 69 additions & 50 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export interface RenderOptions {
disableHtmlSanitization?: boolean;
renderer?: Renderer;
allowedClasses?: { [index: string]: boolean | Array<string | RegExp> };
allowedTags?: string[];
allowedAttributes?: Record<string, sanitizeHtml.AllowedAttribute[]>;
breaks?: boolean;
}

Expand Down Expand Up @@ -143,7 +145,7 @@ export function render(markdown: string, opts: RenderOptions = {}): string {
return html;
}

let allowedTags = sanitizeHtml.defaults.allowedTags.concat([
let defaultAllowedTags = sanitizeHtml.defaults.allowedTags.concat([
"img",
"video",
"svg",
Expand All @@ -156,10 +158,10 @@ export function render(markdown: string, opts: RenderOptions = {}): string {
"summary",
]);
if (opts.allowIframes) {
allowedTags.push("iframe");
defaultAllowedTags.push("iframe");
}
if (opts.allowMath) {
allowedTags = allowedTags.concat([
defaultAllowedTags = defaultAllowedTags.concat([
"math",
"maction",
"annotation",
Expand Down Expand Up @@ -244,59 +246,76 @@ export function render(markdown: string, opts: RenderOptions = {}): string {
section: ["footnotes"],
};

const defaultAllowedAttributes = {
...sanitizeHtml.defaults.allowedAttributes,
img: ["src", "alt", "height", "width", "align", "title"],
video: [
"src",
"alt",
"height",
"width",
"autoplay",
"muted",
"loop",
"playsinline",
"poster",
"controls",
"title",
],
a: [
"id",
"aria-hidden",
"href",
"tabindex",
"rel",
"target",
"title",
"data-footnote-ref",
"data-footnote-backref",
"aria-label",
"aria-describedby",
],
svg: ["viewbox", "width", "height", "aria-hidden", "background"],
path: ["fill-rule", "d"],
circle: ["cx", "cy", "r", "stroke", "stroke-width", "fill", "alpha"],
span: opts.allowMath ? ["aria-hidden", "style"] : [],
h1: ["id"],
h2: ["id"],
h3: ["id"],
h4: ["id"],
h5: ["id"],
h6: ["id"],
li: ["id"],
td: ["colspan", "rowspan", "align"],
iframe: ["src", "width", "height"], // Only used when iframe tags are allowed in the first place.
math: ["xmlns"], // Only enabled when math is enabled
annotation: ["encoding"], // Only enabled when math is enabled
details: ["open"],
section: ["data-footnotes"],
};

return sanitizeHtml(html, {
transformTags: {
img: transformMedia,
video: transformMedia,
},
allowedTags,
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ["src", "alt", "height", "width", "align", "title"],
video: [
"src",
"alt",
"height",
"width",
"autoplay",
"muted",
"loop",
"playsinline",
"poster",
"controls",
"title",
],
a: [
"id",
"aria-hidden",
"href",
"tabindex",
"rel",
"target",
"title",
"data-footnote-ref",
"data-footnote-backref",
"aria-label",
"aria-describedby",
],
svg: ["viewbox", "width", "height", "aria-hidden", "background"],
path: ["fill-rule", "d"],
circle: ["cx", "cy", "r", "stroke", "stroke-width", "fill", "alpha"],
span: opts.allowMath ? ["aria-hidden", "style"] : [],
h1: ["id"],
h2: ["id"],
h3: ["id"],
h4: ["id"],
h5: ["id"],
h6: ["id"],
li: ["id"],
td: ["colspan", "rowspan", "align"],
iframe: ["src", "width", "height"], // Only used when iframe tags are allowed in the first place.
math: ["xmlns"], // Only enabled when math is enabled
annotation: ["encoding"], // Only enabled when math is enabled
section: ["data-footnotes"],
},
allowedTags: [...defaultAllowedTags, ...opts.allowedTags ?? []],
allowedAttributes: mergeAttributes(
defaultAllowedAttributes,
opts.allowedAttributes ?? {},
),
allowedClasses: { ...defaultAllowedClasses, ...opts.allowedClasses },
allowProtocolRelative: false,
});
}

function mergeAttributes(
defaults: Record<string, sanitizeHtml.AllowedAttribute[]>,
customs: Record<string, sanitizeHtml.AllowedAttribute[]>,
) {
const merged = { ...defaults };
for (const tag in customs) {
merged[tag] = [...(merged[tag] || []), ...customs[tag]];
}
return merged;
}
2 changes: 2 additions & 0 deletions test/fixtures/customAllowedTags.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<p>X Skill: <meter optimum="40" value="20">85%</meter>
<a href="asdf.html" hreflang="de">foo</a></p>
2 changes: 2 additions & 0 deletions test/fixtures/customAllowedTags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
X Skill: <meter optimum="40" high="80" max="100" value="20">85%</meter>
<a href="asdf.html" hreflang="de">foo</a>
11 changes: 11 additions & 0 deletions test/fixtures/detailsSummaryDel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<p>Example</p>
<details open>
<summary>Shopping list</summary>

<ul>
<li>Vegetables</li>
<li>Fruits</li>
<li>Fish</li>
<li><del>tofu</del></li>
</ul>
</details>
38 changes: 38 additions & 0 deletions test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,41 @@ Deno.test("hard line breaks", () => {
const html = render(markdown, { breaks: true });
assertEquals(html, expected);
});

Deno.test(
"custom allowed tags and attributes",
() => {
const markdown = Deno.readTextFileSync(
"./test/fixtures/customAllowedTags.md",
);
const expected = Deno.readTextFileSync(
"./test/fixtures/customAllowedTags.html",
);

const html = render(markdown, {
allowedTags: ["meter"],
allowedAttributes: { meter: ["value", "optimum"], a: ["hreflang"] },
});
assertEquals(html, expected);
},
);

Deno.test("details, summary, and del", () => {
const markdown = `Example
<details open>
<summary>Shopping list</summary>
* Vegetables
* Fruits
* Fish
* <del>tofu</del>
</details>`;
const expected = Deno.readTextFileSync(
"./test/fixtures/detailsSummaryDel.html",
);

const html = render(markdown);
assertEquals(html, expected.trim());
});

0 comments on commit b689881

Please sign in to comment.