Merge pull request #25 from mcndt/markdown-footnotes

feat:  render footnote at bottom of page
This commit is contained in:
Maxime Cannoodt 2022-08-23 13:43:00 +02:00 committed by GitHub
commit 424f9b804d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 147 additions and 3 deletions

View File

@ -1,5 +1,8 @@
# Changelog # Changelog
## [2022-08-23]
- feat: ✨ Footnotes are rendered as they are in the Obsidian client.
## [2022-08-16] ## [2022-08-16]
- fix: 🐛 Fix highlights not rendering correctly when mixed with other formatting. ([issue #19](https://github.com/mcndt/noteshare.space/issues/19)) - fix: 🐛 Fix highlights not rendering correctly when mixed with other formatting. ([issue #19](https://github.com/mcndt/noteshare.space/issues/19))

View File

@ -15,15 +15,24 @@
import MathBlock from '$lib/marked/renderers/MathBlock.svelte'; import MathBlock from '$lib/marked/renderers/MathBlock.svelte';
import ListItem from '$lib/marked/renderers/ListItem.svelte'; import ListItem from '$lib/marked/renderers/ListItem.svelte';
import Code from '$lib/marked/renderers/Code.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; export let plaintext: string;
let ref: HTMLDivElement; let ref: HTMLDivElement;
let footnotes: HTMLDivElement[];
let footnoteContainer: HTMLDivElement;
// @ts-ignore: typing mismatch // @ts-ignore: typing mismatch
marked.use({ extensions: extensions }); marked.use({ extensions: extensions });
const options = { ...marked.defaults, breaks: true }; const options = { ...marked.defaults, breaks: true };
function onParsed() {
setTitle();
parseFootnotes();
}
/** /**
* Searches for the first major header in the document to use as page title. * 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);
});
}
</script> </script>
<div <div
@ -47,7 +69,7 @@ prose-strong:font-bold prose-a:font-normal prose-blockquote:font-normal prose-bl
prose-blockquote:first:before:content-[''] prose-hr:transition-colors" prose-blockquote:first:before:content-[''] prose-hr:transition-colors"
> >
<SvelteMarkdown <SvelteMarkdown
on:parsed={setTitle} on:parsed={onParsed}
renderers={{ renderers={{
heading: Heading, heading: Heading,
list: List, list: List,
@ -60,9 +82,17 @@ prose-blockquote:first:before:content-[''] prose-hr:transition-colors"
blockquote: Blockquote, blockquote: Blockquote,
'math-inline': MathInline, 'math-inline': MathInline,
'math-block': MathBlock, 'math-block': MathBlock,
code: Code code: Code,
'footnote-ref': FootnoteRef,
footnote: Footnote
}} }}
source={plaintext} source={plaintext}
{options} {options}
/> />
<!-- footnote container -->
{#if footnotes?.length > 0}
<hr />
<div bind:this={footnoteContainer} />
{/if}
</div> </div>

View File

@ -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 [ export default [
InternalLinkExtension, InternalLinkExtension,
InternalEmbedExtension, InternalEmbedExtension,
TagExtension, TagExtension,
HighlightExtension, HighlightExtension,
MathBlock, MathBlock,
MathInline MathInline,
footnoteRef,
footnote
]; ];

View File

@ -0,0 +1,35 @@
<script lang="ts">
import { scrollToId } from '$lib/util/scrollToId';
export let id: string;
let content: HTMLElement;
let returnRef: HTMLElement;
let refMoved = false;
$: if (content && returnRef) {
// Find the p element in content move the return link to the end of that p element
const p = content.querySelector('p');
if (p) {
p.appendChild(returnRef);
refMoved = true;
}
}
</script>
<div data-footnote class="flex gap-2 prose-p:my-0">
<p>{id}.</p>
<span bind:this={content} class="">
<slot />
</span>
</div>
<span bind:this={returnRef} class="ml-1 {refMoved ? 'inline' : 'hidden'}"
><a
on:click|preventDefault={() => scrollToId(`footnote-ref-${id}`)}
id="footnote-{id}"
href="#footnote-ref-{id}"
class="no-underline">⮥</a
></span
>

View File

@ -0,0 +1,13 @@
<script lang="ts">
import { scrollToId } from '$lib/util/scrollToId';
export let id: string;
</script>
<sup
><a
on:click|preventDefault={() => scrollToId(`footnote-${id}`)}
id="footnote-ref-{id}"
href="#footnote-{id}">[{id}]</a
></sup
>

View File

@ -0,0 +1,6 @@
export function scrollToId(id: string) {
document.querySelector(`#${id}`)?.scrollIntoView();
// scroll 65px down to avoid the navbar
window.scrollBy(0, -65);
}