@memorysmithapp/markdown-renderer 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Markdown.d.ts +16 -1
- package/dist/Markdown.js +47 -25
- package/dist/NoteBody.js +16 -3
- package/dist/footnotes.d.ts +72 -0
- package/dist/footnotes.js +137 -0
- package/package.json +5 -3
package/dist/Markdown.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PluggableList } from 'unified';
|
|
2
|
+
import { type FootnoteRef } from './footnotes.js';
|
|
1
3
|
/**
|
|
2
4
|
* One run of a note, parsed and drawn: the pipeline of MemorySmith's reading page.
|
|
3
5
|
*
|
|
@@ -16,5 +18,18 @@ export interface MarkdownProps {
|
|
|
16
18
|
readonly source?: string;
|
|
17
19
|
readonly onToggleTask?: (ordinal: number) => void;
|
|
18
20
|
readonly writable?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* How this run takes part in the footnotes of a note drawn in several runs
|
|
23
|
+
* (`footnotes.ts`): the number and id of each of its references, or
|
|
24
|
+
* `'section'` for the pass that draws the note's one section.
|
|
25
|
+
*/
|
|
26
|
+
readonly footnotes?: {
|
|
27
|
+
readonly refs: readonly FootnoteRef[];
|
|
28
|
+
} | 'section';
|
|
19
29
|
}
|
|
20
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The remark plugins of the reading surface, in their order. `planFootnotes` reads a note
|
|
32
|
+
* through the same list, so it counts the references this surface draws and no other.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readingPlugins(titleOf: (kind: string) => string | undefined): PluggableList;
|
|
35
|
+
export declare function Markdown({ children, source, onToggleTask, writable, footnotes }: MarkdownProps): import("react").JSX.Element;
|
package/dist/Markdown.js
CHANGED
|
@@ -16,6 +16,7 @@ import { toUnixNewlines } from './markdown.js';
|
|
|
16
16
|
import { MermaidDiagram } from './MermaidDiagram.js';
|
|
17
17
|
import { remarkCallouts } from './remark-callouts.js';
|
|
18
18
|
import { remarkBlockIds, remarkComments, remarkHighlight, remarkMathDollarRule } from './remark-ring.js';
|
|
19
|
+
import { CLOBBER_PREFIX, rehypeFootnoteRun, rehypeFootnoteSection } from './footnotes.js';
|
|
19
20
|
import { ordinalAt } from './tasklist.js';
|
|
20
21
|
/**
|
|
21
22
|
* An image, with the dimensions the specification puts in its alt text
|
|
@@ -42,8 +43,13 @@ function MarkdownImage({ alt, node: _node, ...rest }) {
|
|
|
42
43
|
function InPageAnchor({ href, children, ...rest }) {
|
|
43
44
|
const follow = (event) => {
|
|
44
45
|
const id = decodeURIComponent(href.slice(1));
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
// The note is the root `NoteBody` draws, since a footnote's way back may
|
|
47
|
+
// cross runs; inside an embed it is the embed's own run. A match inside
|
|
48
|
+
// an embed answers only a link inside that same embed.
|
|
49
|
+
const link = event.currentTarget;
|
|
50
|
+
const embed = link.closest('.embed');
|
|
51
|
+
const note = embed ? link.closest('.markdown') : (link.closest('.markdown[data-theme]') ?? link.closest('.markdown'));
|
|
52
|
+
const target = [...(note?.querySelectorAll(`[id="${CSS.escape(id)}"]`) ?? [])].find((candidate) => candidate.closest('.embed') === embed);
|
|
47
53
|
if (!(target instanceof HTMLElement))
|
|
48
54
|
return;
|
|
49
55
|
event.preventDefault();
|
|
@@ -209,42 +215,58 @@ function dropBox(children) {
|
|
|
209
215
|
return children;
|
|
210
216
|
return children.filter((child) => !(isValidElement(child) && child.type === 'input'));
|
|
211
217
|
}
|
|
212
|
-
|
|
218
|
+
/**
|
|
219
|
+
* The remark plugins of the reading surface, in their order. `planFootnotes` reads a note
|
|
220
|
+
* through the same list, so it counts the references this surface draws and no other.
|
|
221
|
+
*/
|
|
222
|
+
export function readingPlugins(titleOf) {
|
|
223
|
+
return [
|
|
224
|
+
/**
|
|
225
|
+
* `singleTilde: false` is a decision and not a default.
|
|
226
|
+
*
|
|
227
|
+
* GFM specifies strikethrough as `~~x~~`; GitHub also accepts a
|
|
228
|
+
* single tilde, outside its own specification. Here that extension
|
|
229
|
+
* actively misrenders a notation the specification leaves out:
|
|
230
|
+
* somebody writing `H~2~O` for a subscript would get `H` struck-out
|
|
231
|
+
* `2` `O`, which is a worse answer than nothing. With it off the
|
|
232
|
+
* characters stay on the page, and the author can see they got what
|
|
233
|
+
* the specification says they get, which is nothing.
|
|
234
|
+
*/
|
|
235
|
+
[remarkGfm, { singleTilde: false }],
|
|
236
|
+
[remarkCallouts, { titleOf }],
|
|
237
|
+
remarkHighlight,
|
|
238
|
+
remarkComments,
|
|
239
|
+
remarkBlockIds,
|
|
240
|
+
remarkMath,
|
|
241
|
+
// After remark-math, and it gives back what was never a formula.
|
|
242
|
+
remarkMathDollarRule,
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
export function Markdown({ children, source, onToggleTask, writable = false, footnotes }) {
|
|
213
246
|
const text = toUnixNewlines(children);
|
|
214
247
|
const { strings } = useHost();
|
|
215
248
|
const calloutTitle = (kind) => Object.hasOwn(strings.callouts, kind) ? strings.callouts[kind] : undefined;
|
|
216
|
-
return (_jsx("div", { className: "markdown", children: _jsx(ReactMarkdown, { remarkPlugins:
|
|
217
|
-
/**
|
|
218
|
-
* `singleTilde: false` is a decision and not a default.
|
|
219
|
-
*
|
|
220
|
-
* GFM specifies strikethrough as `~~x~~`; GitHub also accepts a
|
|
221
|
-
* single tilde, outside its own specification. Here that extension
|
|
222
|
-
* actively misrenders a notation the specification leaves out:
|
|
223
|
-
* somebody writing `H~2~O` for a subscript would get `H` struck-out
|
|
224
|
-
* `2` `O`, which is a worse answer than nothing. With it off the
|
|
225
|
-
* characters stay on the page, and the author can see they got what
|
|
226
|
-
* the specification says they get, which is nothing.
|
|
227
|
-
*/
|
|
228
|
-
[remarkGfm, { singleTilde: false }],
|
|
229
|
-
[remarkCallouts, { titleOf: calloutTitle }],
|
|
230
|
-
remarkHighlight,
|
|
231
|
-
remarkComments,
|
|
232
|
-
remarkBlockIds,
|
|
233
|
-
remarkMath,
|
|
234
|
-
// After remark-math, and it gives back what was never a formula.
|
|
235
|
-
remarkMathDollarRule,
|
|
236
|
-
],
|
|
249
|
+
return (_jsx("div", { className: "markdown", children: _jsx(ReactMarkdown, { remarkPlugins: readingPlugins(calloutTitle),
|
|
237
250
|
// Raw HTML is NOT enabled, and its absence is the point: no
|
|
238
251
|
// `rehype-raw` is loaded, so a note carrying `<script>` is text. It is
|
|
239
252
|
// a security boundary rather than a rendering preference, because a
|
|
240
253
|
// notebook is written by several people and by agents (§7.9).
|
|
241
254
|
// A formula KaTeX cannot typeset shows its source, never an error (§7.8).
|
|
242
|
-
rehypePlugins: [
|
|
255
|
+
rehypePlugins: [
|
|
256
|
+
[rehypeKatex, { throwOnError: false }],
|
|
257
|
+
...(footnotes === undefined
|
|
258
|
+
? []
|
|
259
|
+
: footnotes === 'section'
|
|
260
|
+
? [rehypeFootnoteSection]
|
|
261
|
+
: [[rehypeFootnoteRun, footnotes]]),
|
|
262
|
+
],
|
|
243
263
|
// Footnotes (§7.12): the words around them are the reader's, and the
|
|
244
264
|
// label heads the notes gathered at the end of the note, where a
|
|
245
265
|
// reader sees it — GFM hides it, and a stylesheet that hides nothing
|
|
246
266
|
// shows it as a visible heading, so it is in the reader's language.
|
|
247
267
|
remarkRehypeOptions: {
|
|
268
|
+
// remark-rehype's own default, stated: `footnotes.ts` renumbers by it.
|
|
269
|
+
clobberPrefix: CLOBBER_PREFIX,
|
|
248
270
|
footnoteLabel: strings.footnotes,
|
|
249
271
|
footnoteLabelProperties: { className: ['footnotes-label'] },
|
|
250
272
|
footnoteBackLabel: (referenceIndex) => strings.footnoteBack(referenceIndex + 1),
|
package/dist/NoteBody.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { HOST_KEYS, HostProvider, resolveHost } from './host.js';
|
|
4
|
-
import {
|
|
4
|
+
import { planFootnotes } from './footnotes.js';
|
|
5
|
+
import { Markdown, readingPlugins } from './Markdown.js';
|
|
5
6
|
import { bodyStart, remoteImageHosts, resolveWikilinks } from './markdown.js';
|
|
6
7
|
import { taskBoxes } from './tasklist.js';
|
|
7
8
|
import { Transclusion } from './Transclusion.js';
|
|
@@ -64,16 +65,28 @@ export function NoteBody({ markdown, writable = false, onToggleTask, ...host })
|
|
|
64
65
|
* every note would be a thing to scroll past rather than a thing to read.
|
|
65
66
|
*/
|
|
66
67
|
const hosts = resolved.remoteNotice ? remoteImageHosts(body) : [];
|
|
68
|
+
// The footnotes of a note an embed cuts (§7.12): each run is handed the
|
|
69
|
+
// definitions of the others and the numbers of the note, and the note's one
|
|
70
|
+
// section is drawn at its end, below the last embed too. Null, and nothing
|
|
71
|
+
// changes, for a note no embed cuts or with no footnote to carry.
|
|
72
|
+
const runs = segments.flatMap((segment) => ('rendered' in segment ? [segment.rendered] : []));
|
|
73
|
+
const footnotes = useMemo(() => planFootnotes(runs, runs.length < segments.length, readingPlugins(() => undefined)),
|
|
74
|
+
// The runs and whether an embed cut them are the whole input.
|
|
75
|
+
[runs.join('\u0000'), runs.length < segments.length]);
|
|
76
|
+
let run = 0;
|
|
67
77
|
return (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: "markdown", "data-theme": resolved.theme, children: [segments.map((segment, index) => {
|
|
68
78
|
if (!('rendered' in segment)) {
|
|
69
79
|
return _jsx(Transclusion, { target: segment.target, anchor: segment.anchor }, index);
|
|
70
80
|
}
|
|
71
81
|
const base = seen;
|
|
72
82
|
seen += taskBoxes(segment.rendered).length;
|
|
83
|
+
const planned = footnotes?.runs[run++];
|
|
73
84
|
return (_jsx(Markdown, {
|
|
74
85
|
// The text the ordinal is counted over, stated rather than left to
|
|
75
86
|
// a default: it is the one the parser reports offsets into, and
|
|
76
87
|
// `mappable` above is what makes it safe to write back through.
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
// A planned run is the same text with definitions appended after
|
|
89
|
+
// it, so every box keeps the offset it had.
|
|
90
|
+
source: planned?.text ?? segment.rendered, onToggleTask: onToggleTask && ((ordinal) => onToggleTask(base + ordinal)), writable: writable && mappable, ...(planned ? { footnotes: { refs: planned.refs } } : {}), children: planned?.text ?? segment.rendered }, index));
|
|
91
|
+
}), footnotes && _jsx(Markdown, { footnotes: "section", children: footnotes.section }), hosts.length > 0 && _jsx("p", { className: "remote-notice", children: resolved.strings.remoteImages(hosts) })] }) }));
|
|
79
92
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footnotes across the runs of a note (§7.12).
|
|
3
|
+
*
|
|
4
|
+
* `NoteBody` draws the text between two embeds as runs of their own, each
|
|
5
|
+
* through its own parser, and a footnote is resolved inside one parse: GFM
|
|
6
|
+
* reads `[^label]` as a reference only when the same document defines
|
|
7
|
+
* `label`. A reference above an embed and its definition below it therefore
|
|
8
|
+
* never met, and the reference reached the page as its characters. MemorySmith's
|
|
9
|
+
* reading page does the same; this renderer does not carry it.
|
|
10
|
+
*
|
|
11
|
+
* The runs stay runs — they are what keeps the task ordinals and the layout
|
|
12
|
+
* of every other note as MemorySmith draws them — and the note is drawn as one
|
|
13
|
+
* document would draw it:
|
|
14
|
+
*
|
|
15
|
+
* - every run is handed the definitions written in the OTHER runs, appended
|
|
16
|
+
* after its own text, so its references parse; appending after the text
|
|
17
|
+
* moves no offset the task boxes are counted from;
|
|
18
|
+
* - each run's own footnotes section is dropped, and each reference is
|
|
19
|
+
* renumbered in the order of the note, with the id one document would give
|
|
20
|
+
* its n-th use of a label;
|
|
21
|
+
* - one section is drawn at the end of the note, after its last embed too,
|
|
22
|
+
* from every reference of the note in order and every definition, so its
|
|
23
|
+
* numbers and its ways back are the ones a single document would have.
|
|
24
|
+
*
|
|
25
|
+
* Only a definition written at the top level of a run travels. A note with
|
|
26
|
+
* one inside a quote or a list, with no embed, or with no definition, is
|
|
27
|
+
* planned as nothing and drawn exactly as before.
|
|
28
|
+
*/
|
|
29
|
+
import { type PluggableList } from 'unified';
|
|
30
|
+
/** The prefix remark-rehype gives every footnote id, set explicitly where `Markdown` draws. */
|
|
31
|
+
export declare const CLOBBER_PREFIX = "user-content-";
|
|
32
|
+
interface HastNode {
|
|
33
|
+
type: string;
|
|
34
|
+
tagName?: string;
|
|
35
|
+
value?: string;
|
|
36
|
+
properties?: Record<string, unknown>;
|
|
37
|
+
children?: HastNode[];
|
|
38
|
+
}
|
|
39
|
+
/** One reference as the note numbers it: its footnote's number, and which use of the label it is. */
|
|
40
|
+
export interface FootnoteRef {
|
|
41
|
+
readonly number: number;
|
|
42
|
+
readonly occurrence: number;
|
|
43
|
+
}
|
|
44
|
+
export interface FootnotePlan {
|
|
45
|
+
/** For each run, in order: the text its parser reads, and its references in the order it draws them. */
|
|
46
|
+
readonly runs: readonly {
|
|
47
|
+
readonly text: string;
|
|
48
|
+
readonly refs: readonly FootnoteRef[];
|
|
49
|
+
}[];
|
|
50
|
+
/** The text the one footnotes section of the note is drawn from. */
|
|
51
|
+
readonly section: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* How the runs of a note draw their footnotes, or null when nothing has to
|
|
55
|
+
* change: a note no embed cuts, or one with no footnote to carry. `cut` says
|
|
56
|
+
* whether an embed cut it — a note of one run followed by an embed is cut
|
|
57
|
+
* too, because its notes belong below the embed, at the end of the note.
|
|
58
|
+
*
|
|
59
|
+
* `plugins` are the remark plugins of `Markdown`, so a reference the reading
|
|
60
|
+
* surface would not draw — inside a comment, say — is not counted either.
|
|
61
|
+
*/
|
|
62
|
+
export declare function planFootnotes(runs: readonly string[], cut: boolean, plugins: PluggableList): FootnotePlan | null;
|
|
63
|
+
/**
|
|
64
|
+
* The rehype side of a run: its own section dropped, and each reference given
|
|
65
|
+
* the number and the id the note gives it.
|
|
66
|
+
*/
|
|
67
|
+
export declare function rehypeFootnoteRun(options: {
|
|
68
|
+
refs: readonly FootnoteRef[];
|
|
69
|
+
}): (tree: HastNode) => void;
|
|
70
|
+
/** The rehype side of the section pass: the section and nothing else. */
|
|
71
|
+
export declare function rehypeFootnoteSection(): (tree: HastNode) => void;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footnotes across the runs of a note (§7.12).
|
|
3
|
+
*
|
|
4
|
+
* `NoteBody` draws the text between two embeds as runs of their own, each
|
|
5
|
+
* through its own parser, and a footnote is resolved inside one parse: GFM
|
|
6
|
+
* reads `[^label]` as a reference only when the same document defines
|
|
7
|
+
* `label`. A reference above an embed and its definition below it therefore
|
|
8
|
+
* never met, and the reference reached the page as its characters. MemorySmith's
|
|
9
|
+
* reading page does the same; this renderer does not carry it.
|
|
10
|
+
*
|
|
11
|
+
* The runs stay runs — they are what keeps the task ordinals and the layout
|
|
12
|
+
* of every other note as MemorySmith draws them — and the note is drawn as one
|
|
13
|
+
* document would draw it:
|
|
14
|
+
*
|
|
15
|
+
* - every run is handed the definitions written in the OTHER runs, appended
|
|
16
|
+
* after its own text, so its references parse; appending after the text
|
|
17
|
+
* moves no offset the task boxes are counted from;
|
|
18
|
+
* - each run's own footnotes section is dropped, and each reference is
|
|
19
|
+
* renumbered in the order of the note, with the id one document would give
|
|
20
|
+
* its n-th use of a label;
|
|
21
|
+
* - one section is drawn at the end of the note, after its last embed too,
|
|
22
|
+
* from every reference of the note in order and every definition, so its
|
|
23
|
+
* numbers and its ways back are the ones a single document would have.
|
|
24
|
+
*
|
|
25
|
+
* Only a definition written at the top level of a run travels. A note with
|
|
26
|
+
* one inside a quote or a list, with no embed, or with no definition, is
|
|
27
|
+
* planned as nothing and drawn exactly as before.
|
|
28
|
+
*/
|
|
29
|
+
import remarkParse from 'remark-parse';
|
|
30
|
+
import { unified } from 'unified';
|
|
31
|
+
/** The prefix remark-rehype gives every footnote id, set explicitly where `Markdown` draws. */
|
|
32
|
+
export const CLOBBER_PREFIX = 'user-content-';
|
|
33
|
+
/** What one document would key a label by: remark-rehype's own normalisation. */
|
|
34
|
+
const keyOf = (node) => String(node.identifier ?? '').toUpperCase();
|
|
35
|
+
/**
|
|
36
|
+
* How the runs of a note draw their footnotes, or null when nothing has to
|
|
37
|
+
* change: a note no embed cuts, or one with no footnote to carry. `cut` says
|
|
38
|
+
* whether an embed cut it — a note of one run followed by an embed is cut
|
|
39
|
+
* too, because its notes belong below the embed, at the end of the note.
|
|
40
|
+
*
|
|
41
|
+
* `plugins` are the remark plugins of `Markdown`, so a reference the reading
|
|
42
|
+
* surface would not draw — inside a comment, say — is not counted either.
|
|
43
|
+
*/
|
|
44
|
+
export function planFootnotes(runs, cut, plugins) {
|
|
45
|
+
if (!cut || !runs.some((run) => run.includes('[^')))
|
|
46
|
+
return null;
|
|
47
|
+
const processor = unified().use(remarkParse).use(plugins);
|
|
48
|
+
const read = (text) => processor.runSync(processor.parse(text), text);
|
|
49
|
+
// The definitions, the first of each label winning, as in one document.
|
|
50
|
+
const definitions = new Map();
|
|
51
|
+
let nested = false;
|
|
52
|
+
runs.forEach((run, index) => {
|
|
53
|
+
const tree = processor.parse(run);
|
|
54
|
+
const below = (node) => (node.children ?? []).some((child) => child.type === 'footnoteDefinition' || below(child));
|
|
55
|
+
nested ||= (tree.children ?? []).some(below);
|
|
56
|
+
for (const child of tree.children ?? []) {
|
|
57
|
+
const start = child.position?.start?.offset;
|
|
58
|
+
const end = child.position?.end?.offset;
|
|
59
|
+
if (child.type !== 'footnoteDefinition' || start === undefined || end === undefined)
|
|
60
|
+
continue;
|
|
61
|
+
if (!definitions.has(keyOf(child)))
|
|
62
|
+
definitions.set(keyOf(child), { source: run.slice(start, end), run: index });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// A definition inside a quote or a list cannot be lifted into the section
|
|
66
|
+
// without its markers, so a note that has one is drawn as before.
|
|
67
|
+
if (definitions.size === 0 || nested)
|
|
68
|
+
return null;
|
|
69
|
+
const order = [];
|
|
70
|
+
const uses = new Map();
|
|
71
|
+
const labels = [];
|
|
72
|
+
const planned = runs.map((run, index) => {
|
|
73
|
+
const others = [...definitions.values()].filter((definition) => definition.run !== index);
|
|
74
|
+
const text = others.length === 0 ? run : `${run}\n\n${others.map((definition) => definition.source).join('\n\n')}\n`;
|
|
75
|
+
const refs = [];
|
|
76
|
+
const walk = (node) => {
|
|
77
|
+
// A reference inside a definition is drawn in the section, after every
|
|
78
|
+
// reference of the body, which is where the section pass meets it.
|
|
79
|
+
if (node.type === 'footnoteDefinition')
|
|
80
|
+
return;
|
|
81
|
+
if (node.type === 'footnoteReference') {
|
|
82
|
+
const key = keyOf(node);
|
|
83
|
+
if (!order.includes(key))
|
|
84
|
+
order.push(key);
|
|
85
|
+
const occurrence = (uses.get(key) ?? 0) + 1;
|
|
86
|
+
uses.set(key, occurrence);
|
|
87
|
+
labels.push(node.label ?? node.identifier ?? '');
|
|
88
|
+
refs.push({ number: order.indexOf(key) + 1, occurrence });
|
|
89
|
+
}
|
|
90
|
+
for (const child of node.children ?? [])
|
|
91
|
+
walk(child);
|
|
92
|
+
};
|
|
93
|
+
walk(read(text));
|
|
94
|
+
return { text, refs };
|
|
95
|
+
});
|
|
96
|
+
if (order.length === 0)
|
|
97
|
+
return null;
|
|
98
|
+
const section = `${labels.map((label) => `[^${label}]`).join(' ')}\n\n${[...definitions.values()]
|
|
99
|
+
.map((definition) => definition.source)
|
|
100
|
+
.join('\n\n')}\n`;
|
|
101
|
+
return { runs: planned, section };
|
|
102
|
+
}
|
|
103
|
+
function isElement(node, tagName) {
|
|
104
|
+
return node.type === 'element' && node.tagName === tagName;
|
|
105
|
+
}
|
|
106
|
+
const isSection = (node) => isElement(node, 'section') && node.properties?.dataFootnotes !== undefined;
|
|
107
|
+
/**
|
|
108
|
+
* The rehype side of a run: its own section dropped, and each reference given
|
|
109
|
+
* the number and the id the note gives it.
|
|
110
|
+
*/
|
|
111
|
+
export function rehypeFootnoteRun(options) {
|
|
112
|
+
return (tree) => {
|
|
113
|
+
tree.children = (tree.children ?? []).filter((child) => !isSection(child));
|
|
114
|
+
let index = 0;
|
|
115
|
+
const walk = (node) => {
|
|
116
|
+
if (isElement(node, 'a') && node.properties?.dataFootnoteRef !== undefined) {
|
|
117
|
+
const ref = options.refs[index++];
|
|
118
|
+
const href = String(node.properties.href ?? '');
|
|
119
|
+
if (ref && href.startsWith(`#${CLOBBER_PREFIX}fn-`)) {
|
|
120
|
+
const safe = href.slice(`#${CLOBBER_PREFIX}fn-`.length);
|
|
121
|
+
node.properties.id = `${CLOBBER_PREFIX}fnref-${safe}${ref.occurrence > 1 ? `-${ref.occurrence}` : ''}`;
|
|
122
|
+
node.children = [{ type: 'text', value: String(ref.number) }];
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const child of node.children ?? [])
|
|
127
|
+
walk(child);
|
|
128
|
+
};
|
|
129
|
+
walk(tree);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** The rehype side of the section pass: the section and nothing else. */
|
|
133
|
+
export function rehypeFootnoteSection() {
|
|
134
|
+
return (tree) => {
|
|
135
|
+
tree.children = (tree.children ?? []).filter(isSection);
|
|
136
|
+
};
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memorysmithapp/markdown-renderer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A React component that draws a MemorySmith note exactly as MemorySmith's reading page does.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -54,10 +54,12 @@
|
|
|
54
54
|
"refractor": "5.0.0",
|
|
55
55
|
"rehype-katex": "7.0.1",
|
|
56
56
|
"remark-gfm": "4.0.1",
|
|
57
|
-
"remark-math": "6.0.0"
|
|
57
|
+
"remark-math": "6.0.0",
|
|
58
|
+
"remark-parse": "11.0.0",
|
|
59
|
+
"unified": "11.0.5"
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
60
|
-
"@memorysmithapp/markdown-spec": "0.1.
|
|
62
|
+
"@memorysmithapp/markdown-spec": "0.1.2",
|
|
61
63
|
"@types/hast": "3.0.5",
|
|
62
64
|
"@types/react": "19.2.18",
|
|
63
65
|
"@types/react-dom": "19.2.4",
|