diff --git a/webapp/CHANGELOG.md b/webapp/CHANGELOG.md
index 8e603ba..4f6eebe 100644
--- a/webapp/CHANGELOG.md
+++ b/webapp/CHANGELOG.md
@@ -1,5 +1,8 @@
# Changelog
+## [2022-08-23]
+
+- feat: ✨ Footnotes are rendered as they are in the Obsidian client.
## [2022-08-16]
- fix: 🐛 Fix highlights not rendering correctly when mixed with other formatting. ([issue #19](https://github.com/mcndt/noteshare.space/issues/19))
diff --git a/webapp/src/lib/components/MarkdownRenderer.svelte b/webapp/src/lib/components/MarkdownRenderer.svelte
index 60183d8..8073865 100644
--- a/webapp/src/lib/components/MarkdownRenderer.svelte
+++ b/webapp/src/lib/components/MarkdownRenderer.svelte
@@ -15,15 +15,24 @@
import MathBlock from '$lib/marked/renderers/MathBlock.svelte';
import ListItem from '$lib/marked/renderers/ListItem.svelte';
import Code from '$lib/marked/renderers/Code.svelte';
+ import FootnoteRef from '$lib/marked/renderers/FootnoteRef.svelte';
+ import Footnote from '$lib/marked/renderers/Footnote.svelte';
export let plaintext: string;
let ref: HTMLDivElement;
+ let footnotes: HTMLDivElement[];
+ let footnoteContainer: HTMLDivElement;
// @ts-ignore: typing mismatch
marked.use({ extensions: extensions });
const options = { ...marked.defaults, breaks: true };
+ function onParsed() {
+ setTitle();
+ parseFootnotes();
+ }
+
/**
* Searches for the first major header in the document to use as page title.
*/
@@ -37,6 +46,19 @@
}
}
}
+
+ /*
+ * find all elements inside "ref" that have the data-footnote attribute
+ */
+ function parseFootnotes() {
+ footnotes = Array.from(ref.querySelectorAll('[data-footnote]'));
+ }
+
+ $: if (footnotes?.length > 0 && footnoteContainer) {
+ footnotes.forEach((footnote) => {
+ footnoteContainer.appendChild(footnote);
+ });
+ }
+
+
+ {#if footnotes?.length > 0}
+
+
+ {/if}
diff --git a/webapp/src/lib/marked/extensions.ts b/webapp/src/lib/marked/extensions.ts
index 6b3a792..9faddd1 100644
--- a/webapp/src/lib/marked/extensions.ts
+++ b/webapp/src/lib/marked/extensions.ts
@@ -117,11 +117,68 @@ const MathBlock = {
}
};
+const footnoteRef = {
+ name: 'footnote-ref',
+ level: 'inline',
+ start(src: string) {
+ return src.indexOf('[^');
+ },
+
+ tokenizer(src: string) {
+ const match = src.match(/^\[\^([^\]]+)\]/);
+ if (match) {
+ return {
+ type: 'footnote-ref',
+ raw: match[0],
+ id: match[1].trim()
+ };
+ }
+ return false;
+ }
+};
+
+const footnote = {
+ name: 'footnote',
+ level: 'block',
+ start(src: string) {
+ return src.match(/^\[\^([^\]]+)\]:/)?.index;
+ },
+
+ tokenizer(src: string): any {
+ const matchFootnote = /^\[\^([^\]]+)\]: ?((?:[^\r\n]*))/;
+
+ const lines = src.split('\n');
+ const match = lines[0].match(matchFootnote);
+ if (match) {
+ // find all subsequent lines that are not a match with matchFootnote or blank
+ let i = 1;
+ while (i < lines.length && !lines[i].match(matchFootnote) && lines[i].trim() !== '') {
+ i++;
+ }
+ const raw = lines.slice(0, i).join('\n');
+
+ // const text equals raw without the [^id]: part
+ const text = raw.replace(/^\[\^([^\]]+)\]: ?/, '').trim();
+
+ return {
+ type: 'footnote',
+ raw: raw,
+ id: match[1].trim(),
+ // @ts-expect-error - marked types are wrong
+ tokens: this.lexer.blockTokens(text, [])
+ };
+ }
+ return false;
+ }
+};
+
export default [
InternalLinkExtension,
InternalEmbedExtension,
TagExtension,
HighlightExtension,
MathBlock,
- MathInline
+ MathInline,
+ footnoteRef,
+ footnote
];
diff --git a/webapp/src/lib/marked/renderers/Footnote.svelte b/webapp/src/lib/marked/renderers/Footnote.svelte
new file mode 100644
index 0000000..1c19ec4
--- /dev/null
+++ b/webapp/src/lib/marked/renderers/Footnote.svelte
@@ -0,0 +1,35 @@
+
+
+
+
+ scrollToId(`footnote-ref-${id}`)}
+ id="footnote-{id}"
+ href="#footnote-ref-{id}"
+ class="no-underline">⮥
diff --git a/webapp/src/lib/marked/renderers/FootnoteRef.svelte b/webapp/src/lib/marked/renderers/FootnoteRef.svelte
new file mode 100644
index 0000000..3e802fa
--- /dev/null
+++ b/webapp/src/lib/marked/renderers/FootnoteRef.svelte
@@ -0,0 +1,13 @@
+
+
+ scrollToId(`footnote-${id}`)}
+ id="footnote-ref-{id}"
+ href="#footnote-{id}">[{id}]
diff --git a/webapp/src/lib/util/scrollToId.ts b/webapp/src/lib/util/scrollToId.ts
new file mode 100644
index 0000000..28a6565
--- /dev/null
+++ b/webapp/src/lib/util/scrollToId.ts
@@ -0,0 +1,6 @@
+export function scrollToId(id: string) {
+ document.querySelector(`#${id}`)?.scrollIntoView();
+
+ // scroll 65px down to avoid the navbar
+ window.scrollBy(0, -65);
+}