Merge pull request #25 from mcndt/markdown-footnotes
feat: ✨ render footnote at bottom of page
This commit is contained in:
commit
424f9b804d
@ -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))
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<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"
|
||||
>
|
||||
<SvelteMarkdown
|
||||
on:parsed={setTitle}
|
||||
on:parsed={onParsed}
|
||||
renderers={{
|
||||
heading: Heading,
|
||||
list: List,
|
||||
@ -60,9 +82,17 @@ prose-blockquote:first:before:content-[''] prose-hr:transition-colors"
|
||||
blockquote: Blockquote,
|
||||
'math-inline': MathInline,
|
||||
'math-block': MathBlock,
|
||||
code: Code
|
||||
code: Code,
|
||||
'footnote-ref': FootnoteRef,
|
||||
footnote: Footnote
|
||||
}}
|
||||
source={plaintext}
|
||||
{options}
|
||||
/>
|
||||
|
||||
<!-- footnote container -->
|
||||
{#if footnotes?.length > 0}
|
||||
<hr />
|
||||
<div bind:this={footnoteContainer} />
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -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
|
||||
];
|
||||
|
35
webapp/src/lib/marked/renderers/Footnote.svelte
Normal file
35
webapp/src/lib/marked/renderers/Footnote.svelte
Normal 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
|
||||
>
|
13
webapp/src/lib/marked/renderers/FootnoteRef.svelte
Normal file
13
webapp/src/lib/marked/renderers/FootnoteRef.svelte
Normal 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
|
||||
>
|
6
webapp/src/lib/util/scrollToId.ts
Normal file
6
webapp/src/lib/util/scrollToId.ts
Normal 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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user