Skip to content

Commit

Permalink
feat: add footnote support (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
deer authored Jan 29, 2024
1 parent 2582968 commit 1c3f599
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 4 deletions.
2 changes: 2 additions & 0 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export { default as GitHubSlugger } from "https://esm.sh/github-slugger@2.0.0?pi

export { default as markedAlert } from "https://esm.sh/marked-alert@2.0.1?pin=v135";

export { default as markedFootnote } from "https://esm.sh/marked-footnote@1.2.2?pin=v135";

export { gfmHeadingId } from "https://esm.sh/marked-gfm-heading-id@3.1.2?pin=v135";

export { default as Prism } from "https://esm.sh/prismjs@1.29.0?pin=v135";
Expand Down
20 changes: 19 additions & 1 deletion mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
katex,
Marked,
markedAlert,
markedFootnote,
Prism,
sanitizeHtml,
} from "./deps.ts";
Expand All @@ -14,6 +15,7 @@ export { CSS, KATEX_CSS, Marked };

Marked.marked.use(markedAlert());
Marked.marked.use(gfmHeadingId());
Marked.marked.use(markedFootnote());

export class Renderer extends Marked.Renderer {
allowMath: boolean;
Expand Down Expand Up @@ -236,6 +238,8 @@ export function render(markdown: string, opts: RenderOptions = {}): string {
a: ["anchor"],
p: ["markdown-alert-title"],
svg: ["octicon", "octicon-alert", "octicon-link"],
h2: ["sr-only"],
section: ["footnotes"],
};

return sanitizeHtml(html, {
Expand All @@ -260,7 +264,19 @@ export function render(markdown: string, opts: RenderOptions = {}): string {
"controls",
"title",
],
a: ["id", "aria-hidden", "href", "tabindex", "rel", "target", "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"],
Expand All @@ -271,10 +287,12 @@ export function render(markdown: string, opts: RenderOptions = {}): string {
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"],
},
allowedClasses: { ...defaultAllowedClasses, ...opts.allowedClasses },
allowProtocolRelative: false,
Expand Down
2 changes: 1 addition & 1 deletion style.js

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions style/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,22 @@
background-color: var(--color-prettylights-syntax-markup-inserted-bg);
}
}

.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
word-wrap: normal;
border: 0;
}

[data-footnote-ref]::before {
content: '[';
}

[data-footnote-ref]::after {
content: ']';
}
57 changes: 57 additions & 0 deletions test/fixtures/footnote.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<h1 id="example"><a class="anchor" aria-hidden="true" tabindex="-1" href="#example"><svg class="octicon octicon-link" viewbox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a>Example</h1><p>Here is a simple footnote<sup><a id="footnote-ref-1" href="#footnote-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup>.
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> With some additional text after it<sup><a id="footnote-ref-%40%23%24%25" href="#footnote-%40%23%24%25" data-footnote-ref aria-describedby="footnote-label">2</a></sup>
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> and without disrupting the
blocks<sup><a id="footnote-ref-bignote" href="#footnote-bignote" data-footnote-ref aria-describedby="footnote-label">3</a></sup>.
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /></p>
<section class="footnotes" data-footnotes>
<h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="footnote-1">
<p>This is a footnote content. <a href="#footnote-ref-1" data-footnote-backref aria-label="Back to reference 1"></a></p>
</li>
<li id="footnote-%40%23%24%25">
<p>A footnote on the label: "@#$%". <a href="#footnote-ref-%40%23%24%25" data-footnote-backref aria-label="Back to reference @#$%"></a></p>
</li>
<li id="footnote-bignote">
<p>The first paragraph of the definition.</p>
<p>Paragraph two of the definition.</p>
<blockquote>
<p>A blockquote with
multiple lines.</p>
</blockquote>
<pre><code>a code block</code></pre><table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
</thead>
<tbody><tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
</tbody></table>
<p>A `final` paragraph before list.</p>
<ul>
<li>Item 1</li>
<li>Item 2<ul>
<li>Subitem 1</li>
<li>Subitem 2</li>
</ul>
</li>
</ul> <a href="#footnote-ref-bignote" data-footnote-backref aria-label="Back to reference bignote"></a>
</li>
</ol>
</section>
45 changes: 45 additions & 0 deletions test/fixtures/footnote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Example

[^1]: This is a footnote content.

Here is a simple footnote[^1].
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br> With some additional text after it[^@#$%]
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br> and without disrupting the
blocks[^bignote].
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br>

[^bignote]: The first paragraph of the definition.

Paragraph two of the definition.

> A blockquote with
> multiple lines.
~~~
a code block
~~~
| Header 1 | Header 2 |
| -------- | -------- |
| Cell 1 | Cell 2 |
A \`final\` paragraph before list.
- Item 1
- Item 2
- Subitem 1
- Subitem 2
[^@#$%]: A footnote on the label: "@#$%".
77 changes: 76 additions & 1 deletion test/server_test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assert } from "./test_deps.ts";
import { assert, assertEquals } from "./test_deps.ts";
import { browserTest } from "./test_utils.ts";

Deno.test("basic md table with dollar signs", async () => {
Expand Down Expand Up @@ -77,3 +77,78 @@ Deno.test("basic md table with dollar signs", async () => {
);
});
});

Deno.test("footnote with style", async () => {
await browserTest("footnotes", async (page) => {
// 1. Test page jump on clicking footnote links
const scrollPositionBefore = await page.evaluate(() => globalThis.scrollY);
await page.click("#footnote-ref-1"); // click the first footnote link. note that we select by id, not href
const scrollPositionAfter = await page.evaluate(() => globalThis.scrollY);
assert(scrollPositionAfter > scrollPositionBefore);

await page.click("#footnote-ref-bignote");
const scrollPositionAfter2 = await page.evaluate(() => globalThis.scrollY);
assert(scrollPositionAfter2 === scrollPositionAfter);

await page.click("#footnote-1 > p > a");
const scrollPositionAfter3 = await page.evaluate(() => globalThis.scrollY);
assert(scrollPositionAfter3 < scrollPositionAfter2);
assert(scrollPositionAfter3 > scrollPositionBefore);

// 2. Verify footnote link styling
const beforeContent = await page.evaluate(() => {
const element = document.querySelector("#footnote-ref-1");
if (element) {
return globalThis.getComputedStyle(element, "::before").content;
}
return null;
});
const afterContent = await page.evaluate(() => {
const element = document.querySelector("#footnote-ref-1");
if (element) {
return globalThis.getComputedStyle(element, "::after").content;
}
return null;
});
assertEquals(beforeContent, '"["');
assertEquals(afterContent, '"]"');

// 3. Check Visibility of "Footnotes" H2
const h2Style = await page.evaluate(() => {
const element = document.querySelector("#footnote-label");
if (element) {
const computedStyle = globalThis.getComputedStyle(element);
return {
position: computedStyle.position,
width: computedStyle.width,
height: computedStyle.height,
overflow: computedStyle.overflow,
clip: computedStyle.clip,
wordWrap: computedStyle.wordWrap,
border: computedStyle.border,
};
}
return null;
});
assert(h2Style);
assertEquals(h2Style.position, "absolute");
assertEquals(h2Style.width, "1px");
assertEquals(h2Style.height, "1px");
assertEquals(h2Style.overflow, "hidden");
assertEquals(h2Style.clip, "rect(0px, 0px, 0px, 0px)");
assertEquals(h2Style.wordWrap, "normal");
assertEquals(h2Style.border, "");

// 4. Verify blue box around the footnote after clicking
await page.click("#footnote-ref-1");
const footnoteStyle = await page.evaluate(() => {
const element = document.querySelector("#footnote-1");
if (element) {
return globalThis.getComputedStyle(element)
.outlineColor;
}
return null;
});
assertEquals(footnoteStyle, "rgb(31, 35, 40)");
});
});
8 changes: 8 additions & 0 deletions test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,11 @@ Deno.test("render github-slugger not reused", function () {
assertEquals(html, expected);
}
});

Deno.test("footnotes", () => {
const markdown = Deno.readTextFileSync("./test/fixtures/footnote.md");
const expected = Deno.readTextFileSync("./test/fixtures/footnote.html");

const html = render(markdown);
assertEquals(html, expected);
});
5 changes: 4 additions & 1 deletion test/test_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type TestCase = {
renderOptions?: RenderOptions;
};

export type TestCases = "basicMarkdownTable";
export type TestCases = "basicMarkdownTable" | "footnotes";

export const testCases: Record<TestCases, TestCase> = {
"basicMarkdownTable": {
Expand All @@ -18,6 +18,9 @@ export const testCases: Record<TestCases, TestCase> = {
| Grape | 60 | $0.05 | $3.00 |
| Total | | | $16.00 |`,
},
"footnotes": {
markdown: Deno.readTextFileSync("./test/fixtures/footnote.md"),
},
};

export async function browserTest(
Expand Down

0 comments on commit 1c3f599

Please sign in to comment.