Skip to content

Commit

Permalink
feat: Convert footnotes HTML like Pandoc
Browse files Browse the repository at this point in the history
  • Loading branch information
akabekobeko committed May 14, 2021
1 parent d439d82 commit 146abea
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 25 deletions.
18 changes: 9 additions & 9 deletions docs/vfm.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ To specify multiple classes, define as `class:'foo bar'`.

## Footnotes

Define a footnote.
Define a footnotes, like [Pandoc](https://pandoc.org/MANUAL.html#footnotes).

**VFM**

Expand All @@ -467,18 +467,18 @@ Footnotes can also be written inline ^[This part is a footnote.].

```html
<p>
VFM is developed in the GitHub repository <sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>.
Issues are managed on GitHub <sup id="fnref-issues"><a href="#fn-issues" class="footnote-ref">Issues</a></sup>.
Footnotes can also be written inline <sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>.
VFM is developed in the GitHub repository <a id="fnref1" href="#fn1" class="footnote-ref" role="doc-noteref"><sup>1</sup></a>.
Issues are managed on GitHub <a id="fnref2" href="#fn2" class="footnote-ref" role="doc-noteref"><sup>2</sup></a>.
Footnotes can also be written inline <a id="fnref3" href="#fn3" class="footnote-ref" role="doc-noteref"><sup>3</sup></a>.
</p>
<div class="footnotes">
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn-1"><a href="https://github.com/vivliostyle/vfm">VFM</a><a href="#fnref-1" class="footnote-backref">↩</a></li>
<li id="fn-issues"><a href="https://github.com/vivliostyle/vfm/issues">Issues</a><a href="#fnref-issues" class="footnote-backref">↩</a></li>
<li id="fn-2">This part is a footnote.<a href="#fnref-2" class="footnote-backref">↩</a></li>
<li id="fn1" role="doc-endnote"><a href="https://github.com/vivliostyle/vfm">VFM</a><a href="#fnref1" class="footnote-back" role="doc-backlink">↩</a></li>
<li id="fn2" role="doc-endnote"><a href="https://github.com/vivliostyle/vfm/issues">Issues</a><a href="#fnref2" class="footnote-back" role="doc-backlink">↩</a></li>
<li id="fn3" role="doc-endnote">This part is a footnote.<a href="#fnref3" class="footnote-back" role="doc-backlink">↩</a></li>
</ol>
</div>
</section>
```

**CSS**
Expand Down
105 changes: 105 additions & 0 deletions src/plugins/footnotes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,111 @@
import { Element, Node } from 'hast';
import { select, selectAll } from 'hast-util-select';
import footnotes from 'remark-footnotes';

/**
* Replace the footnote link with Pandoc format.
* @param tree Tree of Hypertext AST.
*/
const replaceFootnoteLinks = (tree: Node) => {
const sups = (selectAll('sup[id^="fnref-"]', tree) as Element[]).filter(
(node) => node.children.length === 1 && node.children[0].tagName === 'a',
);

for (let i = 0; i < sups.length; ++i) {
const parent = sups[i];
const refIndex = i + 1;
parent.tagName = 'a';
parent.properties = {
id: `fnref${refIndex}`,
href: `#fn${refIndex}`,
className: ['footnote-ref'],
role: 'doc-noteref',
};

const child = parent.children[0];
child.tagName = 'sup';
child.properties = {};
child.children = [{ type: 'text', value: `${refIndex}` }];
}
};

/**
* Check if it has a class name as a back reference.
* @param className Array of class names.
* @returns `true` for back reference, `false` otherwise.
*/
const hasBackReferenceClass = (className: any) => {
if (Array.isArray(className)) {
for (const name of className) {
if (name === 'footnote-backref') {
return true;
}
}
}

return false;
};

/**
* Replace back reference with Pandoc format.
* @param elements Children elements of footnote.
* @param index Index of footnote.
*/
const replaceBackReference = (elements: any[], index: number) => {
for (const element of elements) {
if (
element.type === 'element' &&
element.tagName === 'a' &&
element.properties &&
hasBackReferenceClass(element.properties.className)
) {
element.properties.href = `#fnref${index}`;
element.properties.className = ['footnote-back'];
element.properties.role = 'doc-backlink';

// Back reference is only one
break;
}
}
};

/**
* Replace the footnote with Pandoc format.
* @param tree Tree of Hypertext AST.
*/
const replaceFootnotes = (tree: Node) => {
const area = select('div.footnotes', tree) as Element | undefined;
if (area && area.properties) {
area.tagName = 'section';
area.properties.role = 'doc-endnotes';
} else {
return;
}

const items = selectAll('section.footnotes ol li', tree) as Element[];
for (let i = 0; i < items.length; ++i) {
const item = items[i];
if (!item.properties) {
continue;
}

const refIndex = i + 1;
item.properties.id = `fn${refIndex}`;
item.properties.role = 'doc-endnote';
replaceBackReference(item.children, refIndex);
}
};

/**
* Process Markdown AST.
*/
export const mdast = [footnotes, { inlineNotes: true }];

/**
* Process math related Hypertext AST.
* Resolves HTML diffs between `remark-footnotes` and Pandoc footnotes.
*/
export const hast = () => (tree: Node) => {
replaceFootnoteLinks(tree);
replaceFootnotes(tree);
};
2 changes: 2 additions & 0 deletions src/revive-rehype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import remark2rehype from 'remark-rehype';
import unified from 'unified';
import { handler as code } from './plugins/code';
import { hast as figure } from './plugins/figure';
import { hast as footnotes } from './plugins/footnotes';
import {
handlerDisplayMath as displayMath,
handlerInlineMath as inlineMath,
Expand Down Expand Up @@ -30,5 +31,6 @@ export const reviveRehype = [
],
raw,
figure,
footnotes,
inspect('hast'),
] as unified.PluggableList<unified.Settings>;
32 changes: 16 additions & 16 deletions tests/footnotes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ it('Footnotes', () => {
[^1]: [VFM](https://github.com/vivliostyle/vfm)`;
const received = stringify(md, { partial: true });
const expected = `
<p>VFM is developed in the GitHub repository <sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>.</p>
<div class="footnotes">
<p>VFM is developed in the GitHub repository <a id="fnref1" href="#fn1" class="footnote-ref" role="doc-noteref"><sup>1</sup></a>.</p>
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn-1"><a href="https://github.com/vivliostyle/vfm">VFM</a><a href="#fnref-1" class="footnote-backref">↩</a></li>
<li id="fn1" role="doc-endnote"><a href="https://github.com/vivliostyle/vfm">VFM</a><a href="#fnref1" class="footnote-back" role="doc-backlink">↩</a></li>
</ol>
</div>
</section>
`;
expect(received).toBe(expected);
});
Expand All @@ -21,13 +21,13 @@ it('Inline', () => {
const md = `Footnotes can also be written inline ^[This part is a footnote.].`;
const received = stringify(md, { partial: true });
const expected = `
<p>Footnotes can also be written inline <sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>.</p>
<div class="footnotes">
<p>Footnotes can also be written inline <a id="fnref1" href="#fn1" class="footnote-ref" role="doc-noteref"><sup>1</sup></a>.</p>
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn-1">This part is a footnote.<a href="#fnref-1" class="footnote-backref">↩</a></li>
<li id="fn1" role="doc-endnote">This part is a footnote.<a href="#fnref1" class="footnote-back" role="doc-backlink">↩</a></li>
</ol>
</div>
</section>
`;
expect(received).toBe(expected);
});
Expand All @@ -44,18 +44,18 @@ Footnotes can also be written inline ^[This part is a footnote.].
const received = stringify(md, { partial: true });
const expected = `
<p>
VFM is developed in the GitHub repository <sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>.
Issues are managed on GitHub <sup id="fnref-issues"><a href="#fn-issues" class="footnote-ref">Issues</a></sup>.
Footnotes can also be written inline <sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>.
VFM is developed in the GitHub repository <a id="fnref1" href="#fn1" class="footnote-ref" role="doc-noteref"><sup>1</sup></a>.
Issues are managed on GitHub <a id="fnref2" href="#fn2" class="footnote-ref" role="doc-noteref"><sup>2</sup></a>.
Footnotes can also be written inline <a id="fnref3" href="#fn3" class="footnote-ref" role="doc-noteref"><sup>3</sup></a>.
</p>
<div class="footnotes">
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn-1"><a href="https://github.com/vivliostyle/vfm">VFM</a><a href="#fnref-1" class="footnote-backref">↩</a></li>
<li id="fn-issues"><a href="https://github.com/vivliostyle/vfm/issues">Issues</a><a href="#fnref-issues" class="footnote-backref">↩</a></li>
<li id="fn-2">This part is a footnote.<a href="#fnref-2" class="footnote-backref">↩</a></li>
<li id="fn1" role="doc-endnote"><a href="https://github.com/vivliostyle/vfm">VFM</a><a href="#fnref1" class="footnote-back" role="doc-backlink">↩</a></li>
<li id="fn2" role="doc-endnote"><a href="https://github.com/vivliostyle/vfm/issues">Issues</a><a href="#fnref2" class="footnote-back" role="doc-backlink">↩</a></li>
<li id="fn3" role="doc-endnote">This part is a footnote.<a href="#fnref3" class="footnote-back" role="doc-backlink">↩</a></li>
</ol>
</div>
</section>
`;
expect(received).toBe(expected);
});

0 comments on commit 146abea

Please sign in to comment.