Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add footnote support #94

Merged
merged 5 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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