@memorysmithapp/markdown-renderer 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +37 -2
- package/dist/CopyButton.d.ts +27 -0
- package/dist/CopyButton.js +69 -0
- package/dist/LinkChoice.d.ts +28 -0
- package/dist/LinkChoice.js +181 -0
- package/dist/Markdown.d.ts +35 -0
- package/dist/Markdown.js +283 -0
- package/dist/MermaidDiagram.d.ts +16 -0
- package/dist/MermaidDiagram.js +70 -0
- package/dist/NoteBody.d.ts +15 -0
- package/dist/NoteBody.js +92 -0
- package/dist/Transclusion.d.ts +11 -0
- package/dist/Transclusion.js +66 -0
- package/dist/address.d.ts +53 -0
- package/dist/address.js +69 -0
- package/dist/delegated.d.ts +39 -0
- package/dist/delegated.js +63 -0
- package/dist/footnotes.d.ts +72 -0
- package/dist/footnotes.js +137 -0
- package/dist/highlight.d.ts +54 -0
- package/dist/highlight.js +96 -0
- package/dist/host.d.ts +135 -0
- package/dist/host.js +49 -0
- package/dist/image-dimensions.d.ts +33 -0
- package/dist/image-dimensions.js +42 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/markdown.d.ts +68 -0
- package/dist/markdown.js +395 -0
- package/dist/remark-callouts.d.ts +40 -0
- package/dist/remark-callouts.js +108 -0
- package/dist/remark-ring.d.ts +68 -0
- package/dist/remark-ring.js +178 -0
- package/dist/strings.d.ts +55 -0
- package/dist/strings.js +64 -0
- package/dist/tasklist.d.ts +25 -0
- package/dist/tasklist.js +80 -0
- package/dist/transclusion.d.ts +76 -0
- package/dist/transclusion.js +161 -0
- package/dist/wikilink-address.d.ts +8 -0
- package/dist/wikilink-address.js +13 -0
- package/package.json +75 -3
- package/styles.css +865 -0
package/dist/address.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which addresses the reading surface will follow.
|
|
3
|
+
*
|
|
4
|
+
* `react-markdown` normally passes every URL through a filter that keeps the
|
|
5
|
+
* safe schemes and drops the rest, and the reading page once had it **switched
|
|
6
|
+
* off** — `urlTransform={(url) => url}`. There was a reason: an unresolved
|
|
7
|
+
* wikilink is rendered as `[text](pending:target)`, and the stock filter does
|
|
8
|
+
* not know `pending:`, so it erased the pending link from the page. A whole
|
|
9
|
+
* protection was traded for one scheme.
|
|
10
|
+
*
|
|
11
|
+
* This is that trade undone. The list says what passes, never what is
|
|
12
|
+
* refused, because a notebook is written by several people and by agents, and a
|
|
13
|
+
* denylist is a promise to have thought of everything.
|
|
14
|
+
*
|
|
15
|
+
* **What passes, and why:**
|
|
16
|
+
*
|
|
17
|
+
* - `pending:` — ours, and the reason the filter was off in the first place;
|
|
18
|
+
* and `attachment:` and `choice:`, ours too.
|
|
19
|
+
* - A path of the application (`/`), a relative one (`./`, `../`) and an
|
|
20
|
+
* anchor of the page (`#`).
|
|
21
|
+
* - `http:` and `https:` — the web, which is what a note cites.
|
|
22
|
+
* - `mailto:` — a person, which is what a note cites next most often.
|
|
23
|
+
*
|
|
24
|
+
* **What does not, and why each one:**
|
|
25
|
+
*
|
|
26
|
+
* - **`obsidian://`, and any other scheme of an editor. Decided, not
|
|
27
|
+
* overlooked.** It opens the notebook of whoever has that editor installed and
|
|
28
|
+
* that notebook on that machine, so the link works for the person who wrote it
|
|
29
|
+
* and does nothing at all for everybody else who opens the note — a promise
|
|
30
|
+
* the host cannot keep on behalf of a tool it does not run. A notebook is
|
|
31
|
+
* served to whoever reads it, and an address only its author can follow is
|
|
32
|
+
* not an address.
|
|
33
|
+
* - `data:` — a whole page carried inside the address. `data:text/html` is a
|
|
34
|
+
* script running in the reader's session, offered by whoever wrote the note.
|
|
35
|
+
* - `javascript:` and `vbscript:` — script written where an address goes.
|
|
36
|
+
* React blocks the first one on its own; that it does is not a reason to
|
|
37
|
+
* hand it the chance.
|
|
38
|
+
* - `file:` — the disk of whoever is reading, addressed by somebody else.
|
|
39
|
+
*
|
|
40
|
+
* A refused address is not silently swallowed: the text stays on the page and
|
|
41
|
+
* stops being clickable, which is what the anchor of the surface does with an
|
|
42
|
+
* empty href. An author sees they got nothing, the same answer the
|
|
43
|
+
* specification gives to a notation it does not implement.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* `pending:`, `attachment:` and `choice:` are this surface's own: a wikilink
|
|
47
|
+
* that matched no note, a reference to a file the notebook keeps beside its
|
|
48
|
+
* notes, and a wikilink several notes may answer. None of them leaves the page
|
|
49
|
+
* — the anchor of the surface turns each into what it is — and they are listed
|
|
50
|
+
* here so the filter does not empty them into the refusal case, which says
|
|
51
|
+
* something else entirely.
|
|
52
|
+
*/
|
|
53
|
+
const ALLOWED_SCHEME = /^(https?|mailto|pending|attachment|choice):/i;
|
|
54
|
+
/** Anything of the shape `word:`, which is what makes an address absolute. */
|
|
55
|
+
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
56
|
+
/**
|
|
57
|
+
* The address as it will be written, or the empty string when this surface
|
|
58
|
+
* will not follow it.
|
|
59
|
+
*
|
|
60
|
+
* An address with no scheme is a path, an anchor or a relative target, and it
|
|
61
|
+
* cannot reach outside the page on its own: those pass untouched, which is
|
|
62
|
+
* what keeps `./note.md`, `/v/a-notebook/note/x` and `#a-heading` working.
|
|
63
|
+
*/
|
|
64
|
+
export function followable(url) {
|
|
65
|
+
const trimmed = url.trim();
|
|
66
|
+
if (!HAS_SCHEME.test(trimmed))
|
|
67
|
+
return url;
|
|
68
|
+
return ALLOWED_SCHEME.test(trimmed) ? url : '';
|
|
69
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entries of the specification whose rendering this renderer does NOT
|
|
3
|
+
* assert, because the base parser is what produces them.
|
|
4
|
+
*
|
|
5
|
+
* **This list is the renderer's decision, and it used to be a field.** Until
|
|
6
|
+
* specification v0.4.0 every entry carried a `ring`, and the reading-surface
|
|
7
|
+
* expectations were scoped to everything outside the `base` one. The
|
|
8
|
+
* specification dropped the tier for a good reason: an implementation is asked
|
|
9
|
+
* for the notation the document lists and not for a specification in full. But
|
|
10
|
+
* the reason the guard was scoped did not go away with the field, so the scope
|
|
11
|
+
* is written here instead of being inferred from data that no longer says it.
|
|
12
|
+
*
|
|
13
|
+
* The reason: asserting that emphasis renders as `<em>` is a claim about
|
|
14
|
+
* react-markdown and not about this surface. CommonMark is the floor every
|
|
15
|
+
* renderer already stands on; what the conformance test proves is what the
|
|
16
|
+
* specification adds on top of it.
|
|
17
|
+
*
|
|
18
|
+
* **Two things this list is not.**
|
|
19
|
+
*
|
|
20
|
+
* It is not a claim that these forms are untested: the crossings — where the
|
|
21
|
+
* specification changes what a base form MEANS, such as a link inside a code
|
|
22
|
+
* span, an embed that looks like an image, or `---` under a paragraph — are
|
|
23
|
+
* asserted by hand in the conformance test.
|
|
24
|
+
*
|
|
25
|
+
* And it is not allowed to silently absorb a new entry. The conformance test
|
|
26
|
+
* asserts that every `reading-surface` entry is either expected there or listed
|
|
27
|
+
* here, so a form the specification adds in a later version fails the build
|
|
28
|
+
* until somebody classifies it.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DELEGATED_TO_THE_BASE_PARSER: ReadonlySet<string>;
|
|
31
|
+
/**
|
|
32
|
+
* The `reading-surface` entries of a specification that are neither asserted
|
|
33
|
+
* nor delegated: the ones a renderer would silently not implement. Empty is the
|
|
34
|
+
* only answer a conforming renderer gives.
|
|
35
|
+
*/
|
|
36
|
+
export declare function unclassified(notations: ReadonlyArray<{
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly reader: string;
|
|
39
|
+
}>, asserted: ReadonlySet<string>, delegated?: ReadonlySet<string>): string[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entries of the specification whose rendering this renderer does NOT
|
|
3
|
+
* assert, because the base parser is what produces them.
|
|
4
|
+
*
|
|
5
|
+
* **This list is the renderer's decision, and it used to be a field.** Until
|
|
6
|
+
* specification v0.4.0 every entry carried a `ring`, and the reading-surface
|
|
7
|
+
* expectations were scoped to everything outside the `base` one. The
|
|
8
|
+
* specification dropped the tier for a good reason: an implementation is asked
|
|
9
|
+
* for the notation the document lists and not for a specification in full. But
|
|
10
|
+
* the reason the guard was scoped did not go away with the field, so the scope
|
|
11
|
+
* is written here instead of being inferred from data that no longer says it.
|
|
12
|
+
*
|
|
13
|
+
* The reason: asserting that emphasis renders as `<em>` is a claim about
|
|
14
|
+
* react-markdown and not about this surface. CommonMark is the floor every
|
|
15
|
+
* renderer already stands on; what the conformance test proves is what the
|
|
16
|
+
* specification adds on top of it.
|
|
17
|
+
*
|
|
18
|
+
* **Two things this list is not.**
|
|
19
|
+
*
|
|
20
|
+
* It is not a claim that these forms are untested: the crossings — where the
|
|
21
|
+
* specification changes what a base form MEANS, such as a link inside a code
|
|
22
|
+
* span, an embed that looks like an image, or `---` under a paragraph — are
|
|
23
|
+
* asserted by hand in the conformance test.
|
|
24
|
+
*
|
|
25
|
+
* And it is not allowed to silently absorb a new entry. The conformance test
|
|
26
|
+
* asserts that every `reading-surface` entry is either expected there or listed
|
|
27
|
+
* here, so a form the specification adds in a later version fails the build
|
|
28
|
+
* until somebody classifies it.
|
|
29
|
+
*/
|
|
30
|
+
export const DELEGATED_TO_THE_BASE_PARSER = new Set([
|
|
31
|
+
'paragraph',
|
|
32
|
+
'backslash-escape',
|
|
33
|
+
'character-reference',
|
|
34
|
+
'heading-atx',
|
|
35
|
+
'heading-setext',
|
|
36
|
+
'thematic-break',
|
|
37
|
+
'block-quote',
|
|
38
|
+
'list-bullet',
|
|
39
|
+
'list-ordered',
|
|
40
|
+
'code-fenced',
|
|
41
|
+
'code-indented',
|
|
42
|
+
'link-reference-definition',
|
|
43
|
+
'code-span',
|
|
44
|
+
'emphasis',
|
|
45
|
+
'strong',
|
|
46
|
+
'strong-emphasis',
|
|
47
|
+
'link-inline',
|
|
48
|
+
'link-reference',
|
|
49
|
+
'image',
|
|
50
|
+
'autolink',
|
|
51
|
+
'hard-line-break',
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* The `reading-surface` entries of a specification that are neither asserted
|
|
55
|
+
* nor delegated: the ones a renderer would silently not implement. Empty is the
|
|
56
|
+
* only answer a conforming renderer gives.
|
|
57
|
+
*/
|
|
58
|
+
export function unclassified(notations, asserted, delegated = DELEGATED_TO_THE_BASE_PARSER) {
|
|
59
|
+
return notations
|
|
60
|
+
.filter((entry) => entry.reader === 'reading-surface')
|
|
61
|
+
.map((entry) => entry.id)
|
|
62
|
+
.filter((id) => !asserted.has(id) && !delegated.has(id));
|
|
63
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax highlighting for a fenced block that names its language.
|
|
3
|
+
*
|
|
4
|
+
* **This is presentation and nothing else.** The specification attaches a rendering
|
|
5
|
+
* rule to exactly one info string, `mermaid`, and says of every other one only
|
|
6
|
+
* that it names the language by convention. So highlighting is a product
|
|
7
|
+
* decision rather than a conformance obligation — §8 is explicit that an
|
|
8
|
+
* implementation MAY render an undescribed form however it likes — and the
|
|
9
|
+
* four rules below are the ones the specification *does* impose on anything sitting
|
|
10
|
+
* this close to the body of a note.
|
|
11
|
+
*
|
|
12
|
+
* **The bytes never change.** Highlighting is display. §7.6 sets the principle
|
|
13
|
+
* for comments — the bytes stay in the file and a tool that returns the note
|
|
14
|
+
* returns them — and §7.11 states it as behaviour, where a task toggle writes
|
|
15
|
+
* back exactly the one character that changed. Nothing here touches the
|
|
16
|
+
* source: the tokens are read from it and the string is not rewritten.
|
|
17
|
+
*
|
|
18
|
+
* **An unknown language renders as plain code, never as nothing.** The specification
|
|
19
|
+
* sets that pattern twice, for the diagram it cannot draw (§7.2) and for the
|
|
20
|
+
* mathematics it cannot typeset (§7.8): show the source, never hide it. A
|
|
21
|
+
* language `refractor` does not carry falls through to exactly what was
|
|
22
|
+
* rendered before this existed.
|
|
23
|
+
*
|
|
24
|
+
* **`mermaid` keeps its branch**, and it is checked before this is reached. It
|
|
25
|
+
* is the one info string with a rendering rule, and a highlighter installed in
|
|
26
|
+
* front of that check would swallow a diagram.
|
|
27
|
+
*
|
|
28
|
+
* **The markup arrives as elements and never as an HTML string.** §7.9 refuses
|
|
29
|
+
* raw HTML as a security boundary — a notebook is written by several people and
|
|
30
|
+
* by agents, and a page that renders arbitrary HTML out of one is a script
|
|
31
|
+
* injection whose trigger is written by whoever wrote the note. A highlighter
|
|
32
|
+
* returning HTML to be injected reopens that door with the note body as its
|
|
33
|
+
* input. `refractor` produces a hast tree, which is converted to React
|
|
34
|
+
* elements, so nothing is ever parsed as markup.
|
|
35
|
+
*
|
|
36
|
+
* **The languages are enumerated rather than bundled.** Prism carries close to
|
|
37
|
+
* three hundred grammars and a knowledge notebook writes in a handful. The list
|
|
38
|
+
* is what a notebook of MemorySmith plausibly holds — the languages of its own
|
|
39
|
+
* repository, the two query languages, the three configuration formats and the
|
|
40
|
+
* two shells — and adding one is a line, which is the point: a bundle nobody
|
|
41
|
+
* chose is a bundle nobody can defend. It is MemorySmith's list, unchanged, so
|
|
42
|
+
* a fence is highlighted here exactly when it is highlighted there.
|
|
43
|
+
*/
|
|
44
|
+
import type { Root } from 'hast';
|
|
45
|
+
/**
|
|
46
|
+
* The language of a fenced block, from the class react-markdown puts on it.
|
|
47
|
+
*
|
|
48
|
+
* `null` for a block with no info string, for one whose language is not
|
|
49
|
+
* carried, and for `mermaid` — which never reaches here, and is refused
|
|
50
|
+
* explicitly so that a future caller cannot make it.
|
|
51
|
+
*/
|
|
52
|
+
export declare function highlightLanguage(className: string | undefined): string | null;
|
|
53
|
+
/** The tokens of `code` in `language`, as a hast tree. Never an HTML string. */
|
|
54
|
+
export declare function highlightTree(code: string, language: string): Root;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax highlighting for a fenced block that names its language.
|
|
3
|
+
*
|
|
4
|
+
* **This is presentation and nothing else.** The specification attaches a rendering
|
|
5
|
+
* rule to exactly one info string, `mermaid`, and says of every other one only
|
|
6
|
+
* that it names the language by convention. So highlighting is a product
|
|
7
|
+
* decision rather than a conformance obligation — §8 is explicit that an
|
|
8
|
+
* implementation MAY render an undescribed form however it likes — and the
|
|
9
|
+
* four rules below are the ones the specification *does* impose on anything sitting
|
|
10
|
+
* this close to the body of a note.
|
|
11
|
+
*
|
|
12
|
+
* **The bytes never change.** Highlighting is display. §7.6 sets the principle
|
|
13
|
+
* for comments — the bytes stay in the file and a tool that returns the note
|
|
14
|
+
* returns them — and §7.11 states it as behaviour, where a task toggle writes
|
|
15
|
+
* back exactly the one character that changed. Nothing here touches the
|
|
16
|
+
* source: the tokens are read from it and the string is not rewritten.
|
|
17
|
+
*
|
|
18
|
+
* **An unknown language renders as plain code, never as nothing.** The specification
|
|
19
|
+
* sets that pattern twice, for the diagram it cannot draw (§7.2) and for the
|
|
20
|
+
* mathematics it cannot typeset (§7.8): show the source, never hide it. A
|
|
21
|
+
* language `refractor` does not carry falls through to exactly what was
|
|
22
|
+
* rendered before this existed.
|
|
23
|
+
*
|
|
24
|
+
* **`mermaid` keeps its branch**, and it is checked before this is reached. It
|
|
25
|
+
* is the one info string with a rendering rule, and a highlighter installed in
|
|
26
|
+
* front of that check would swallow a diagram.
|
|
27
|
+
*
|
|
28
|
+
* **The markup arrives as elements and never as an HTML string.** §7.9 refuses
|
|
29
|
+
* raw HTML as a security boundary — a notebook is written by several people and
|
|
30
|
+
* by agents, and a page that renders arbitrary HTML out of one is a script
|
|
31
|
+
* injection whose trigger is written by whoever wrote the note. A highlighter
|
|
32
|
+
* returning HTML to be injected reopens that door with the note body as its
|
|
33
|
+
* input. `refractor` produces a hast tree, which is converted to React
|
|
34
|
+
* elements, so nothing is ever parsed as markup.
|
|
35
|
+
*
|
|
36
|
+
* **The languages are enumerated rather than bundled.** Prism carries close to
|
|
37
|
+
* three hundred grammars and a knowledge notebook writes in a handful. The list
|
|
38
|
+
* is what a notebook of MemorySmith plausibly holds — the languages of its own
|
|
39
|
+
* repository, the two query languages, the three configuration formats and the
|
|
40
|
+
* two shells — and adding one is a line, which is the point: a bundle nobody
|
|
41
|
+
* chose is a bundle nobody can defend. It is MemorySmith's list, unchanged, so
|
|
42
|
+
* a fence is highlighted here exactly when it is highlighted there.
|
|
43
|
+
*/
|
|
44
|
+
import { refractor } from 'refractor/core';
|
|
45
|
+
import bash from 'refractor/bash';
|
|
46
|
+
import css from 'refractor/css';
|
|
47
|
+
import diff from 'refractor/diff';
|
|
48
|
+
import graphql from 'refractor/graphql';
|
|
49
|
+
import ini from 'refractor/ini';
|
|
50
|
+
import java from 'refractor/java';
|
|
51
|
+
import javascript from 'refractor/javascript';
|
|
52
|
+
import json from 'refractor/json';
|
|
53
|
+
import markdown from 'refractor/markdown';
|
|
54
|
+
import python from 'refractor/python';
|
|
55
|
+
import sql from 'refractor/sql';
|
|
56
|
+
import toml from 'refractor/toml';
|
|
57
|
+
import tsx from 'refractor/tsx';
|
|
58
|
+
import typescript from 'refractor/typescript';
|
|
59
|
+
import yaml from 'refractor/yaml';
|
|
60
|
+
for (const language of [
|
|
61
|
+
bash,
|
|
62
|
+
css,
|
|
63
|
+
diff,
|
|
64
|
+
graphql,
|
|
65
|
+
ini,
|
|
66
|
+
java,
|
|
67
|
+
javascript,
|
|
68
|
+
json,
|
|
69
|
+
markdown,
|
|
70
|
+
python,
|
|
71
|
+
sql,
|
|
72
|
+
toml,
|
|
73
|
+
tsx,
|
|
74
|
+
typescript,
|
|
75
|
+
yaml,
|
|
76
|
+
]) {
|
|
77
|
+
refractor.register(language);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The language of a fenced block, from the class react-markdown puts on it.
|
|
81
|
+
*
|
|
82
|
+
* `null` for a block with no info string, for one whose language is not
|
|
83
|
+
* carried, and for `mermaid` — which never reaches here, and is refused
|
|
84
|
+
* explicitly so that a future caller cannot make it.
|
|
85
|
+
*/
|
|
86
|
+
export function highlightLanguage(className) {
|
|
87
|
+
const match = /(?:^|\s)language-([A-Za-z0-9#+._-]+)/.exec(className ?? '');
|
|
88
|
+
const named = match?.[1]?.toLowerCase();
|
|
89
|
+
if (!named || named === 'mermaid')
|
|
90
|
+
return null;
|
|
91
|
+
return refractor.registered(named) ? named : null;
|
|
92
|
+
}
|
|
93
|
+
/** The tokens of `code` in `language`, as a hast tree. Never an HTML string. */
|
|
94
|
+
export function highlightTree(code, language) {
|
|
95
|
+
return refractor.highlight(code, language);
|
|
96
|
+
}
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type NoteStrings } from './strings.js';
|
|
3
|
+
/**
|
|
4
|
+
* The host contract: everything the renderer needs from the notebook a note lives in, given by
|
|
5
|
+
* whoever draws it.
|
|
6
|
+
*
|
|
7
|
+
* The package knows no API, no router, no i18n library and no query cache. What MemorySmith reads
|
|
8
|
+
* from those reaches the component as these props: functions that answer about the notebook, and
|
|
9
|
+
* the words the page shows. The host decides; the package draws.
|
|
10
|
+
*
|
|
11
|
+
* **Every default is the safe one.** With no `resolve`, every link is pending; with no `keeps`, no
|
|
12
|
+
* embed reaches a file; with no `renderLink`, an internal address is a plain anchor; with no
|
|
13
|
+
* `strings`, the words are MemorySmith's English.
|
|
14
|
+
*/
|
|
15
|
+
export interface NoteHost {
|
|
16
|
+
/**
|
|
17
|
+
* The address of the one note a link target names, compared after NFC, or `null` when no single
|
|
18
|
+
* note answers it. The target is a name, literal: never slugified, never stripped of an extension.
|
|
19
|
+
*/
|
|
20
|
+
readonly resolve?: (name: string) => string | null;
|
|
21
|
+
/**
|
|
22
|
+
* How many notes a target reaches, by name or, when no name matches, by alias (§5.2). One is a
|
|
23
|
+
* link; more than one is a choice; none is pending. Defaults to one when `resolve` answers and
|
|
24
|
+
* none otherwise.
|
|
25
|
+
*/
|
|
26
|
+
readonly reaches?: (name: string) => number;
|
|
27
|
+
/**
|
|
28
|
+
* The notes a target no single note answers may lead to, asked when its link is clicked. With it,
|
|
29
|
+
* such a link opens the choice in place — or goes straight to the note, when one answers by
|
|
30
|
+
* alias. Without it, the link is pending and asks nothing.
|
|
31
|
+
*/
|
|
32
|
+
readonly choose?: (name: string) => Promise<LinkChoices>;
|
|
33
|
+
/** The address of the host's page about a target, which a choice link carries for a new tab. */
|
|
34
|
+
readonly targetAddress?: (name: string) => string | null;
|
|
35
|
+
/** Goes to an internal address, through the host's router. Defaults to `location.assign`. */
|
|
36
|
+
readonly navigate?: (address: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* The note a target names, for an embed to draw in place: its body as the host keeps it, its name
|
|
39
|
+
* and its address. Asked only for a target exactly one note reaches; `null` draws the embed as
|
|
40
|
+
* pending. Without it, every embed of a note is pending. The package never fetches.
|
|
41
|
+
*/
|
|
42
|
+
readonly loadNote?: (name: string) => Promise<EmbeddedNote | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the reader may tick the task boxes of this note. Defaults to `false`: every box shows
|
|
45
|
+
* its state and none answers a click.
|
|
46
|
+
*/
|
|
47
|
+
readonly writable?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* A task box was ticked. The ordinal counts the boxes of the note as the host gave it, frontmatter
|
|
50
|
+
* included; the host applies it with `toggleTaskAt`, a one-character change of its bytes. The
|
|
51
|
+
* package never writes the note.
|
|
52
|
+
*/
|
|
53
|
+
readonly onToggleTask?: (ordinal: number) => void;
|
|
54
|
+
/** Whether the notebook keeps a file under that name. Only an embed reaches a file. */
|
|
55
|
+
readonly keeps?: (name: string) => boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Draws a file the notebook keeps where a note embeds it: an image, audio, video, or a card. The
|
|
58
|
+
* dimensions are the ones the embed declared, in CSS pixels, or `null`.
|
|
59
|
+
*/
|
|
60
|
+
readonly renderAttachment?: (file: AttachmentRequest) => ReactNode;
|
|
61
|
+
/** Draws a link to an internal address, through the host's router. */
|
|
62
|
+
readonly renderLink?: (link: LinkRequest) => ReactNode;
|
|
63
|
+
/** The words the page shows, in the reader's language; each one left out is MemorySmith's English. */
|
|
64
|
+
readonly strings?: Partial<NoteStrings>;
|
|
65
|
+
/** The theme the host draws in, which picks the theme of a Mermaid diagram. Defaults to `light`. */
|
|
66
|
+
readonly theme?: 'light' | 'dark';
|
|
67
|
+
/**
|
|
68
|
+
* Whether a note that fetched images from other sites says so, naming them, under the body
|
|
69
|
+
* (§7.10). Defaults to `true`: the specification forbids only silence. A host that discloses it
|
|
70
|
+
* elsewhere turns it off and reads the hosts with `remoteImageHosts`.
|
|
71
|
+
*/
|
|
72
|
+
readonly remoteNotice?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** What a target that no single note answers may lead to. */
|
|
75
|
+
export interface LinkChoices {
|
|
76
|
+
/** What matched: the names of notes, their aliases, or nothing. */
|
|
77
|
+
readonly by: 'name' | 'alias' | null;
|
|
78
|
+
readonly notes: readonly LinkChoiceNote[];
|
|
79
|
+
}
|
|
80
|
+
/** A note a choice offers. */
|
|
81
|
+
export interface LinkChoiceNote {
|
|
82
|
+
/** Its name, or `null` for a note that has none. */
|
|
83
|
+
readonly name: string | null;
|
|
84
|
+
/** Its address, which the host follows. */
|
|
85
|
+
readonly address: string;
|
|
86
|
+
/** The folders from the root down to the one that holds it. */
|
|
87
|
+
readonly trail: readonly string[];
|
|
88
|
+
/** What the folder that holds it is for, in the notebook's words. */
|
|
89
|
+
readonly folderDescription: string;
|
|
90
|
+
}
|
|
91
|
+
/** A note an embed draws in place. */
|
|
92
|
+
export interface EmbeddedNote {
|
|
93
|
+
/** The note as the host keeps it, frontmatter included. */
|
|
94
|
+
readonly markdown: string;
|
|
95
|
+
/** Its name, or `null` for a note that has none. */
|
|
96
|
+
readonly name: string | null;
|
|
97
|
+
/** Its address, which the caption under the embed links to, or `null`. */
|
|
98
|
+
readonly address: string | null;
|
|
99
|
+
}
|
|
100
|
+
/** A file to draw: its name, and the dimensions the embed declared. */
|
|
101
|
+
export interface AttachmentRequest {
|
|
102
|
+
readonly name: string;
|
|
103
|
+
readonly width: number | null;
|
|
104
|
+
readonly height: number | null;
|
|
105
|
+
}
|
|
106
|
+
/** A link to draw through the host's router. */
|
|
107
|
+
export interface LinkRequest {
|
|
108
|
+
readonly href: string;
|
|
109
|
+
readonly className?: string;
|
|
110
|
+
readonly children: ReactNode;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Every member of the contract, by name. The `satisfies` is what makes it complete: a member added
|
|
114
|
+
* to `NoteHost` and not listed here does not compile.
|
|
115
|
+
*/
|
|
116
|
+
export declare const HOST_KEYS: (keyof NoteHost)[];
|
|
117
|
+
/** The host contract with every default applied. */
|
|
118
|
+
export interface ResolvedHost {
|
|
119
|
+
readonly resolve: (name: string) => string | null;
|
|
120
|
+
readonly reaches: (name: string) => number;
|
|
121
|
+
readonly choose: ((name: string) => Promise<LinkChoices>) | null;
|
|
122
|
+
readonly targetAddress: (name: string) => string | null;
|
|
123
|
+
readonly navigate: (address: string) => void;
|
|
124
|
+
readonly remoteNotice: boolean;
|
|
125
|
+
readonly loadNote: ((name: string) => Promise<EmbeddedNote | null>) | null;
|
|
126
|
+
readonly keeps: (name: string) => boolean;
|
|
127
|
+
readonly renderAttachment: ((file: AttachmentRequest) => ReactNode) | null;
|
|
128
|
+
readonly renderLink: (link: LinkRequest) => ReactNode;
|
|
129
|
+
readonly strings: NoteStrings;
|
|
130
|
+
readonly theme: 'light' | 'dark';
|
|
131
|
+
}
|
|
132
|
+
export declare function resolveHost(host: NoteHost): ResolvedHost;
|
|
133
|
+
export declare const HostProvider: import("react").Provider<ResolvedHost>;
|
|
134
|
+
/** The host the note is drawn under, read by every part of the surface. */
|
|
135
|
+
export declare function useHost(): ResolvedHost;
|