From 05bd82df2497523d5e0388ecb53ca9b58cbb9e7d Mon Sep 17 00:00:00 2001 From: Maxime Cannoodt Date: Tue, 23 Aug 2022 11:15:11 +0200 Subject: [PATCH] feat: :sparkles: render footnote at bottom of page --- webapp/CHANGELOG.md | 3 + .../lib/components/MarkdownRenderer.svelte | 34 ++++++++++- webapp/src/lib/marked/extensions.ts | 59 ++++++++++++++++++- .../src/lib/marked/renderers/Footnote.svelte | 35 +++++++++++ .../lib/marked/renderers/FootnoteRef.svelte | 13 ++++ webapp/src/lib/util/scrollToId.ts | 6 ++ 6 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 webapp/src/lib/marked/renderers/Footnote.svelte create mode 100644 webapp/src/lib/marked/renderers/FootnoteRef.svelte create mode 100644 webapp/src/lib/util/scrollToId.ts 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 @@ + + +
+

{id}.

+ + + +
+ + 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); +}