@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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +37 -2
  3. package/dist/CopyButton.d.ts +27 -0
  4. package/dist/CopyButton.js +69 -0
  5. package/dist/LinkChoice.d.ts +28 -0
  6. package/dist/LinkChoice.js +181 -0
  7. package/dist/Markdown.d.ts +35 -0
  8. package/dist/Markdown.js +283 -0
  9. package/dist/MermaidDiagram.d.ts +16 -0
  10. package/dist/MermaidDiagram.js +70 -0
  11. package/dist/NoteBody.d.ts +15 -0
  12. package/dist/NoteBody.js +92 -0
  13. package/dist/Transclusion.d.ts +11 -0
  14. package/dist/Transclusion.js +66 -0
  15. package/dist/address.d.ts +53 -0
  16. package/dist/address.js +69 -0
  17. package/dist/delegated.d.ts +39 -0
  18. package/dist/delegated.js +63 -0
  19. package/dist/footnotes.d.ts +72 -0
  20. package/dist/footnotes.js +137 -0
  21. package/dist/highlight.d.ts +54 -0
  22. package/dist/highlight.js +96 -0
  23. package/dist/host.d.ts +135 -0
  24. package/dist/host.js +49 -0
  25. package/dist/image-dimensions.d.ts +33 -0
  26. package/dist/image-dimensions.js +42 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.js +8 -0
  29. package/dist/markdown.d.ts +68 -0
  30. package/dist/markdown.js +395 -0
  31. package/dist/remark-callouts.d.ts +40 -0
  32. package/dist/remark-callouts.js +108 -0
  33. package/dist/remark-ring.d.ts +68 -0
  34. package/dist/remark-ring.js +178 -0
  35. package/dist/strings.d.ts +55 -0
  36. package/dist/strings.js +64 -0
  37. package/dist/tasklist.d.ts +25 -0
  38. package/dist/tasklist.js +80 -0
  39. package/dist/transclusion.d.ts +76 -0
  40. package/dist/transclusion.js +161 -0
  41. package/dist/wikilink-address.d.ts +8 -0
  42. package/dist/wikilink-address.js +13 -0
  43. package/package.json +75 -3
  44. package/styles.css +865 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Notebook callouts, rendered as callouts.
3
+ *
4
+ * A callout is a blockquote whose first line is a type marker:
5
+ *
6
+ * > [!warning] O que pode dar errado
7
+ * > O corpo do aviso.
8
+ *
9
+ * It is a notebook convention and not universal Markdown (§7.1). What
10
+ * MemorySmith's reading page once did was a pass over the
11
+ * string that swapped the marker for an emoji and left the blockquote behind,
12
+ * which reads as a quotation with a picture in front of it. This turns it into
13
+ * the element the vault editors draw, and the type travels to the DOM as an
14
+ * attribute so the stylesheet can colour and ice it without a class per type.
15
+ *
16
+ * It is a remark plugin rather than another pass over the string because the
17
+ * marker has to leave the tree the renderer walks, not the text the parser
18
+ * reads: a `[!note]` inside a code fence is not a callout, and only the parser
19
+ * can tell the difference.
20
+ */
21
+ /** `[!type]`, an optional fold marker, and whatever titles it. */
22
+ const MARKER = /^\[!([A-Za-z][\w-]*)\][+-]?[ \t]*(.*)$/;
23
+ export function remarkCallouts(options = {}) {
24
+ return (tree) => descend(tree, options);
25
+ }
26
+ function descend(node, options) {
27
+ for (const child of node.children ?? []) {
28
+ if (child.type === 'blockquote')
29
+ rewrite(child, options);
30
+ descend(child, options);
31
+ }
32
+ }
33
+ function rewrite(quote, options) {
34
+ const [head, ...blocks] = quote.children ?? [];
35
+ if (head?.type !== 'paragraph')
36
+ return;
37
+ const [lead, ...inline] = head.children ?? [];
38
+ if (lead?.type !== 'text' || typeof lead.value !== 'string')
39
+ return;
40
+ const broke = lead.value.indexOf('\n');
41
+ const marker = MARKER.exec(broke === -1 ? lead.value : lead.value.slice(0, broke));
42
+ if (!marker)
43
+ return;
44
+ const kind = (marker[1] ?? '').toLowerCase();
45
+ const { title, body } = split(marker[2] ?? '', broke === -1 ? null : lead.value.slice(broke + 1), inline);
46
+ quote.data = {
47
+ hName: 'div',
48
+ hProperties: { className: ['callout'], 'data-callout': kind },
49
+ };
50
+ quote.children = [
51
+ {
52
+ type: 'paragraph',
53
+ data: { hName: 'div', hProperties: { className: ['callout-title'] } },
54
+ // An untitled callout is titled by its own type, as the editors do, and
55
+ // in the locale of the reader rather than always in English.
56
+ children: title.length > 0
57
+ ? title
58
+ : [{ type: 'text', value: options.titleOf?.(kind) ?? capitalize(kind) }],
59
+ },
60
+ ...(body.length > 0 ? [{ type: 'paragraph', children: body }] : []),
61
+ ...blocks,
62
+ ];
63
+ }
64
+ /**
65
+ * Cuts the first paragraph at its first line break: what comes before titles
66
+ * the callout, what comes after opens its body. The break can fall inside any
67
+ * of the inline nodes, because a title that carries emphasis makes the
68
+ * paragraph three children and not one, so the split walks them instead of
69
+ * assuming the title ends where the first text node does.
70
+ */
71
+ function split(titled, after, inline) {
72
+ const title = [{ type: 'text', value: titled }];
73
+ const body = [];
74
+ // The lead text already broke, so everything after it belongs to the body.
75
+ let titling = after === null;
76
+ if (after !== null)
77
+ body.push({ type: 'text', value: after });
78
+ for (const node of inline) {
79
+ if (!titling) {
80
+ body.push(node);
81
+ continue;
82
+ }
83
+ const breakAt = node.type === 'text' ? (node.value ?? '').indexOf('\n') : -1;
84
+ if (breakAt === -1) {
85
+ title.push(node);
86
+ continue;
87
+ }
88
+ title.push({ type: 'text', value: (node.value ?? '').slice(0, breakAt) });
89
+ body.push({ type: 'text', value: (node.value ?? '').slice(breakAt + 1) });
90
+ titling = false;
91
+ }
92
+ return { title: trimEdges(title), body: trimEdges(body) };
93
+ }
94
+ /** Drops the empty text nodes the cut leaves at either end. */
95
+ function trimEdges(nodes) {
96
+ const kept = [...nodes];
97
+ while (kept.length > 0 && isBlank(kept[0]))
98
+ kept.shift();
99
+ while (kept.length > 0 && isBlank(kept[kept.length - 1]))
100
+ kept.pop();
101
+ return kept;
102
+ }
103
+ function isBlank(node) {
104
+ return node?.type === 'text' && (node.value ?? '').trim().length === 0;
105
+ }
106
+ function capitalize(value) {
107
+ return value.charAt(0).toUpperCase() + value.slice(1);
108
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The rest of the MemorySmith notation, on the reading surface: marked text,
3
+ * comments, block identifiers (§7.5, §7.6, §7.7) and the dollar rule of
4
+ * mathematics (§7.8).
5
+ *
6
+ * All three are remark plugins rather than passes over the string, for the
7
+ * reason `remark-callouts.ts` already gives: a `==` inside a code fence is not
8
+ * a highlight, and only the parser can tell the difference. Rewriting the text
9
+ * before it is parsed is how a notation starts firing inside examples.
10
+ *
11
+ * None of them touches the bytes. The reading surface is a rendering of the
12
+ * note and never a rewriting of it, so what a host stores and returns is
13
+ * unaffected by everything here — which is the whole of what makes
14
+ * `%%comment%%` an asymmetry that has to be declared rather than a deletion.
15
+ */
16
+ interface MdastNode {
17
+ type: string;
18
+ value?: string;
19
+ children?: MdastNode[];
20
+ data?: {
21
+ hName?: string;
22
+ hProperties?: Record<string, unknown>;
23
+ };
24
+ }
25
+ export declare function remarkHighlight(): (tree: MdastNode) => void;
26
+ export declare function remarkComments(): (tree: MdastNode) => void;
27
+ export declare function remarkBlockIds(): (tree: MdastNode) => void;
28
+ /**
29
+ * The `$` that is not math (§7.8).
30
+ *
31
+ * `remark-math` opens a formula at any `$` and closes it at the next one, so
32
+ * **two prices in one paragraph become a formula**: `costs $30 a month, and
33
+ * the plan $60` renders `30 a month, and the plan` as mathematics. That is not
34
+ * an exotic case, it is a sentence somebody writes on an ordinary day.
35
+ *
36
+ * The specification states four prohibitions, and **each one has an outside edge and
37
+ * an inside edge**. A `$` does not OPEN a formula when it is followed by
38
+ * whitespace, nor when it is immediately preceded by an alphanumeric; it does
39
+ * not CLOSE one when it is preceded by whitespace, nor when it is immediately
40
+ * followed by a digit.
41
+ *
42
+ * The inside edges arrived with specification v0.4.0, and they are what keeps a
43
+ * price whole. `R$ 100` is safe on the outside edge alone, because of the
44
+ * space. `R$100 e o frete R$200` is not: neither `$` sits next to a space, so
45
+ * with only the outside edge the sentence loses its middle — `100 e o frete R`
46
+ * typeset as mathematics, in italic serif, with the `R` and the `200` stranded
47
+ * either side of it. It is an ordinary sentence in pt-BR, where a price is
48
+ * written `R$`.
49
+ *
50
+ * **The cost is deliberate and the specification names it.** An inline formula
51
+ * written immediately after a word character no longer opens, so `2$x$` is
52
+ * text and `2 $x$` is the form that works. A `$` written for any other purpose
53
+ * belongs in a code span, which is opaque to everything — and for `$HOME` and
54
+ * `$PATH` in one sentence that is the only thing that protects them, because
55
+ * both delimiters look legal by every local rule.
56
+ *
57
+ * This runs after `remark-math` and gives back to the text any inline formula
58
+ * that breaks the rule, reading the source the parser read: the delimiters are
59
+ * gone from the node, and the position is what still knows where they were.
60
+ * The characters ON either side of the range are what the inside edges need,
61
+ * which is why the slice reaches one character past each end.
62
+ *
63
+ * Block math (`$$…$$`) is untouched: its delimiters are unambiguous.
64
+ */
65
+ export declare function remarkMathDollarRule(): (tree: MdastNode, file: {
66
+ value?: unknown;
67
+ }) => void;
68
+ export {};
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The rest of the MemorySmith notation, on the reading surface: marked text,
3
+ * comments, block identifiers (§7.5, §7.6, §7.7) and the dollar rule of
4
+ * mathematics (§7.8).
5
+ *
6
+ * All three are remark plugins rather than passes over the string, for the
7
+ * reason `remark-callouts.ts` already gives: a `==` inside a code fence is not
8
+ * a highlight, and only the parser can tell the difference. Rewriting the text
9
+ * before it is parsed is how a notation starts firing inside examples.
10
+ *
11
+ * None of them touches the bytes. The reading surface is a rendering of the
12
+ * note and never a rewriting of it, so what a host stores and returns is
13
+ * unaffected by everything here — which is the whole of what makes
14
+ * `%%comment%%` an asymmetry that has to be declared rather than a deletion.
15
+ */
16
+ /** Walks every node, letting the visitor rewrite a parent's children. */
17
+ function walk(node, visit) {
18
+ if (!node.children)
19
+ return;
20
+ visit(node);
21
+ for (const child of node.children)
22
+ walk(child, visit);
23
+ }
24
+ /** Text nodes carry the raw characters; nothing else is rewritten. */
25
+ function isText(node) {
26
+ return node.type === 'text';
27
+ }
28
+ /**
29
+ * `==highlight==` becomes marked text (§7.5).
30
+ *
31
+ * It carries no meaning beyond emphasis: no edge, no attribute, no index
32
+ * entry. The `==` never reaches the screen.
33
+ */
34
+ const HIGHLIGHT = /==([^=]+)==/g;
35
+ export function remarkHighlight() {
36
+ return (tree) => walk(tree, (parent) => {
37
+ parent.children = (parent.children ?? []).flatMap((child) => {
38
+ if (!isText(child) || !child.value?.includes('=='))
39
+ return [child];
40
+ return split(child.value, HIGHLIGHT, (inner) => ({
41
+ type: 'emphasis',
42
+ data: { hName: 'mark' },
43
+ children: [{ type: 'text', value: inner }],
44
+ }));
45
+ });
46
+ });
47
+ }
48
+ /**
49
+ * `%%comment%%` leaves the page and stays in the file (§7.6).
50
+ *
51
+ * The asymmetry is the decision and it is declared, not discovered: the bytes
52
+ * are untouched, so whatever returns the note returns the comment, and an
53
+ * agent therefore sees what a person on the page does not. Text the
54
+ * author did not want *read on the page* is still text the author wrote, and
55
+ * deleting bytes to make a page tidier is not something this product does.
56
+ */
57
+ const COMMENT = /%%([\s\S]*?)%%/g;
58
+ export function remarkComments() {
59
+ return (tree) => walk(tree, (parent) => {
60
+ parent.children = (parent.children ?? []).flatMap((child) => {
61
+ if (!isText(child) || !child.value?.includes('%%'))
62
+ return [child];
63
+ const stripped = child.value.replace(COMMENT, '');
64
+ // A text that was nothing but a comment leaves no empty text node
65
+ // behind. The paragraph that held it stays, empty, as it does on
66
+ // MemorySmith's reading page.
67
+ return stripped.length === 0 ? [] : [{ ...child, value: stripped }];
68
+ });
69
+ });
70
+ }
71
+ /**
72
+ * `^identifier` at the end of a block names that block (§7.7), and is never
73
+ * rendered as text. What it names is resolved by the embed, not here: this
74
+ * plugin only keeps the marker off the page.
75
+ */
76
+ const BLOCK_ID = /[ \t]*\^([A-Za-z0-9-]+)[ \t]*$/;
77
+ export function remarkBlockIds() {
78
+ return (tree) => walk(tree, (parent) => {
79
+ const children = parent.children ?? [];
80
+ const last = children[children.length - 1];
81
+ if (!last || !isText(last) || !last.value?.includes('^'))
82
+ return;
83
+ const stripped = last.value.replace(BLOCK_ID, '');
84
+ if (stripped === last.value)
85
+ return;
86
+ if (stripped.length === 0)
87
+ children.pop();
88
+ else
89
+ last.value = stripped;
90
+ });
91
+ }
92
+ /**
93
+ * The `$` that is not math (§7.8).
94
+ *
95
+ * `remark-math` opens a formula at any `$` and closes it at the next one, so
96
+ * **two prices in one paragraph become a formula**: `costs $30 a month, and
97
+ * the plan $60` renders `30 a month, and the plan` as mathematics. That is not
98
+ * an exotic case, it is a sentence somebody writes on an ordinary day.
99
+ *
100
+ * The specification states four prohibitions, and **each one has an outside edge and
101
+ * an inside edge**. A `$` does not OPEN a formula when it is followed by
102
+ * whitespace, nor when it is immediately preceded by an alphanumeric; it does
103
+ * not CLOSE one when it is preceded by whitespace, nor when it is immediately
104
+ * followed by a digit.
105
+ *
106
+ * The inside edges arrived with specification v0.4.0, and they are what keeps a
107
+ * price whole. `R$ 100` is safe on the outside edge alone, because of the
108
+ * space. `R$100 e o frete R$200` is not: neither `$` sits next to a space, so
109
+ * with only the outside edge the sentence loses its middle — `100 e o frete R`
110
+ * typeset as mathematics, in italic serif, with the `R` and the `200` stranded
111
+ * either side of it. It is an ordinary sentence in pt-BR, where a price is
112
+ * written `R$`.
113
+ *
114
+ * **The cost is deliberate and the specification names it.** An inline formula
115
+ * written immediately after a word character no longer opens, so `2$x$` is
116
+ * text and `2 $x$` is the form that works. A `$` written for any other purpose
117
+ * belongs in a code span, which is opaque to everything — and for `$HOME` and
118
+ * `$PATH` in one sentence that is the only thing that protects them, because
119
+ * both delimiters look legal by every local rule.
120
+ *
121
+ * This runs after `remark-math` and gives back to the text any inline formula
122
+ * that breaks the rule, reading the source the parser read: the delimiters are
123
+ * gone from the node, and the position is what still knows where they were.
124
+ * The characters ON either side of the range are what the inside edges need,
125
+ * which is why the slice reaches one character past each end.
126
+ *
127
+ * Block math (`$$…$$`) is untouched: its delimiters are unambiguous.
128
+ */
129
+ export function remarkMathDollarRule() {
130
+ return (tree, file) => {
131
+ const source = typeof file.value === 'string' ? file.value : String(file.value ?? '');
132
+ if (source.length === 0)
133
+ return;
134
+ walk(tree, (parent) => {
135
+ parent.children = (parent.children ?? []).map((child) => {
136
+ if (child.type !== 'inlineMath')
137
+ return child;
138
+ const { start, end } = child.position ?? {};
139
+ if (start?.offset === undefined || end?.offset === undefined)
140
+ return child;
141
+ const raw = source.slice(start.offset, end.offset);
142
+ const inner = raw.replace(/^\$+/, '').replace(/\$+$/, '');
143
+ if (inner.length === 0)
144
+ return { type: 'text', value: raw };
145
+ // The outside edges: what sits just inside each delimiter.
146
+ if (/^\s/.test(inner) || /\s$/.test(inner))
147
+ return { type: 'text', value: raw };
148
+ // The inside edges: what sits just OUTSIDE each delimiter, which is
149
+ // the half `R$100 e o frete R$200` is decided by.
150
+ const before = start.offset > 0 ? (source[start.offset - 1] ?? '') : '';
151
+ const after = source[end.offset] ?? '';
152
+ if (before !== '' && /[0-9A-Za-z]/.test(before))
153
+ return { type: 'text', value: raw };
154
+ if (after !== '' && /[0-9]/.test(after))
155
+ return { type: 'text', value: raw };
156
+ return child;
157
+ });
158
+ });
159
+ };
160
+ }
161
+ /** Cuts a text node on a pattern, keeping the parts between the matches. */
162
+ function split(value, pattern, make) {
163
+ const parts = [];
164
+ let cursor = 0;
165
+ pattern.lastIndex = 0;
166
+ for (const match of value.matchAll(pattern)) {
167
+ const at = match.index ?? 0;
168
+ if (at > cursor)
169
+ parts.push({ type: 'text', value: value.slice(cursor, at) });
170
+ parts.push(make(match[1] ?? ''));
171
+ cursor = at + match[0].length;
172
+ }
173
+ if (parts.length === 0)
174
+ return [{ type: 'text', value }];
175
+ if (cursor < value.length)
176
+ parts.push({ type: 'text', value: value.slice(cursor) });
177
+ return parts;
178
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The words the reading surface shows of its own, around what a note says.
3
+ *
4
+ * The package carries no i18n library: the host gives the words in the language of whoever
5
+ * reads, and every word it leaves out falls back to the English MemorySmith ships. A callout
6
+ * kind with no title in `callouts` titles itself, capitalised, as the vault editors do.
7
+ */
8
+ export interface NoteStrings {
9
+ /** The title of an untitled callout, by kind (`note`, `warning`, …). */
10
+ readonly callouts: Readonly<Record<string, string>>;
11
+ /** The heading of the footnotes gathered at the end of a note. */
12
+ readonly footnotes: string;
13
+ /** The label of the way back from a footnote to its reference, counted from 1. */
14
+ readonly footnoteBack: (number: number) => string;
15
+ /** The title of a link no note answers yet. */
16
+ readonly pendingLink: string;
17
+ /** The title of an address the surface does not follow. */
18
+ readonly refusedLink: string;
19
+ /** The title of an embed naming a file the notebook does not keep. */
20
+ readonly attachmentMissing: string;
21
+ /** The verb of the button that copies a block of code. */
22
+ readonly copy: string;
23
+ /** What that button says once the block is copied. */
24
+ readonly copied: string;
25
+ /** The notice under a note that fetched images from other sites, naming them. */
26
+ readonly remoteImages: (hosts: readonly string[]) => string;
27
+ /** The accessible name of the choice a link opens. */
28
+ readonly linkChoice: (target: string) => string;
29
+ /** The heading of the choice when no note answers the target yet. */
30
+ readonly linkLeadsNowhere: string;
31
+ /** The heading of the choice when several notes answer it. */
32
+ readonly linkLeadsToSeveral: string;
33
+ /** What the choice says when no note answers the target. */
34
+ readonly targetPending: (target: string) => string;
35
+ /** What it says when notes are named the target. */
36
+ readonly targetNamed: (target: string, count: number) => string;
37
+ /** What it says when notes carry the target as an alias. */
38
+ readonly targetAliased: (target: string, count: number) => string;
39
+ /** What an embed says when the section or block it names is not in the note. */
40
+ readonly embedSectionMissing: (section: string) => string;
41
+ /** The caption of a note that has no name. */
42
+ readonly unnamedNote: string;
43
+ /** What a region says while it waits for the host. */
44
+ readonly loading: string;
45
+ /** What a region says when the host failed to answer. */
46
+ readonly failed: string;
47
+ /** The control that closes the choice. */
48
+ readonly close: string;
49
+ /** The button that dismisses the choice. */
50
+ readonly cancel: string;
51
+ }
52
+ /** The words MemorySmith shows in English, used for every word a host does not give. */
53
+ export declare const ENGLISH: NoteStrings;
54
+ /** The host's words over the English ones, key by key. */
55
+ export declare function withDefaults(strings: Partial<NoteStrings> | undefined): NoteStrings;
@@ -0,0 +1,64 @@
1
+ /** The callout titles of MemorySmith, in English. */
2
+ const CALLOUTS = {
3
+ note: 'Note',
4
+ info: 'Info',
5
+ todo: 'To do',
6
+ abstract: 'Abstract',
7
+ summary: 'Summary',
8
+ tldr: 'TL;DR',
9
+ tip: 'Tip',
10
+ hint: 'Hint',
11
+ important: 'Important',
12
+ success: 'Success',
13
+ check: 'Check',
14
+ done: 'Done',
15
+ question: 'Question',
16
+ help: 'Help',
17
+ faq: 'FAQ',
18
+ warning: 'Warning',
19
+ caution: 'Caution',
20
+ attention: 'Attention',
21
+ failure: 'Failure',
22
+ fail: 'Fail',
23
+ missing: 'Missing',
24
+ danger: 'Danger',
25
+ error: 'Error',
26
+ bug: 'Bug',
27
+ example: 'Example',
28
+ quote: 'Quote',
29
+ cite: 'Cite',
30
+ };
31
+ /** The words MemorySmith shows in English, used for every word a host does not give. */
32
+ export const ENGLISH = {
33
+ callouts: CALLOUTS,
34
+ footnotes: 'Notes',
35
+ footnoteBack: (number) => `Back to reference ${number}`,
36
+ pendingLink: 'Pending link: the target note does not exist yet.',
37
+ refusedLink: 'This address is not one the reading surface follows, so the text stays and the link does not.',
38
+ attachmentMissing: 'This notebook keeps no file under that name.',
39
+ copy: 'Copy',
40
+ copied: 'Copied',
41
+ remoteImages: (hosts) => hosts.length === 1
42
+ ? `Opening this note fetched an image from ${hosts.join(', ')}, so that site was told your address and the moment you read this.`
43
+ : `Opening this note fetched images from ${hosts.length} other sites — ${hosts.join(', ')} — so each of them was told your address and the moment you read this.`,
44
+ linkChoice: (target) => `The notes the link “${target}” may lead to`,
45
+ linkLeadsNowhere: 'This link leads nowhere yet',
46
+ linkLeadsToSeveral: 'This link leads to more than one note',
47
+ targetPending: (target) => `No note carries the name “${target}” yet. The link is kept and resolves on its own when one does.`,
48
+ targetNamed: (target, count) => count === 1
49
+ ? `“${target}” is the name of one note.`
50
+ : `“${target}” is the name of ${count} notes, each in its own folder. Choose which one to open.`,
51
+ targetAliased: (target, count) => count === 1
52
+ ? `No note is named “${target}”. One note carries it as an alias.`
53
+ : `No note is named “${target}”, so it matched ${count} notes that carry it as an alias — and one of them gives the link up the day somebody writes a note under this name.`,
54
+ embedSectionMissing: (section) => `The section "${section}" is not in this note.`,
55
+ unnamedNote: 'Unnamed note',
56
+ loading: 'Loading…',
57
+ failed: 'Something went wrong.',
58
+ close: 'Close',
59
+ cancel: 'Cancel',
60
+ };
61
+ /** The host's words over the English ones, key by key. */
62
+ export function withDefaults(strings) {
63
+ return { ...ENGLISH, ...strings };
64
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Clicking a task box, on the reading surface.
3
+ *
4
+ * The package never writes the note: a click reports the ordinal of the box,
5
+ * and the host applies it with `toggleTaskAt`, which these functions are
6
+ * exported for. The toggle is POSITIONAL over the original text, never a
7
+ * re-serialisation. Exactly one character between brackets changes, and
8
+ * everything else in the document reaches the host's store as the author
9
+ * wrote it: frontmatter, spacing, line breaks and the rest of the item line
10
+ * included. That is a requirement, not elegance: `splitFrontmatter` is lossy
11
+ * by design, because it flattens lists for display, and rebuilding the
12
+ * document from it would hand the notebook a file nobody typed.
13
+ */
14
+ /** Offset of the `[` of every real task box, in document order. */
15
+ export declare function taskBoxes(text: string): number[];
16
+ /** How many task items start before this offset: the ordinal of that item. */
17
+ export declare function ordinalAt(text: string, offset: number): number;
18
+ /**
19
+ * Flips the n-th task box. Returns null when there is no such item, which is
20
+ * what a stale render looks like, and null is the honest answer: writing the
21
+ * wrong box would be worse than not writing.
22
+ */
23
+ export declare function toggleTaskAt(raw: string, ordinal: number): string | null;
24
+ /** Whether the n-th box is checked, for the optimistic state on the screen. */
25
+ export declare function taskCheckedAt(raw: string, ordinal: number): boolean;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Clicking a task box, on the reading surface.
3
+ *
4
+ * The package never writes the note: a click reports the ordinal of the box,
5
+ * and the host applies it with `toggleTaskAt`, which these functions are
6
+ * exported for. The toggle is POSITIONAL over the original text, never a
7
+ * re-serialisation. Exactly one character between brackets changes, and
8
+ * everything else in the document reaches the host's store as the author
9
+ * wrote it: frontmatter, spacing, line breaks and the rest of the item line
10
+ * included. That is a requirement, not elegance: `splitFrontmatter` is lossy
11
+ * by design, because it flattens lists for display, and rebuilding the
12
+ * document from it would hand the notebook a file nobody typed.
13
+ */
14
+ import { fenceRegions } from './markdown.js';
15
+ /**
16
+ * A task item, as GFM defines it, at the start of a list item — after any
17
+ * block quote markers in front of it, because a callout is a block quote and a
18
+ * checklist is what a `[!todo]` callout holds.
19
+ *
20
+ * MemorySmith's reading page counts without them, so a box inside a callout
21
+ * reported the ordinal of the next box outside it, and ticking it ticked that
22
+ * other box. Counting the quoted boxes on both sides — the text the parser saw
23
+ * and the text the host writes — is what keeps the ordinal of every box its
24
+ * own.
25
+ */
26
+ const TASK = /^(?:[ \t]*>[ \t]?)*[ \t]*(?:[-*+]|\d+[.)])[ \t]+\[([ xX])\]/gm;
27
+ /**
28
+ * Blanks out fenced blocks while keeping every offset intact. A task item
29
+ * inside a fence is an example, not a box, and counting it would shift the
30
+ * index and toggle a different item: the worst possible defect here, because
31
+ * it is silent and lands in the wrong place.
32
+ *
33
+ * The fences are the ones the reading surface finds, by the rules of the
34
+ * specification: a longer outer fence holds a shorter inner one, so a box
35
+ * written after the inner fence closes is still an example.
36
+ */
37
+ function maskFences(text) {
38
+ let masked = '';
39
+ let cursor = 0;
40
+ for (const [start, end] of fenceRegions(text)) {
41
+ masked += text.slice(cursor, start) + text.slice(start, end).replace(/[^\n]/g, ' ');
42
+ cursor = end;
43
+ }
44
+ return masked + text.slice(cursor);
45
+ }
46
+ /** Offset of the `[` of every real task box, in document order. */
47
+ export function taskBoxes(text) {
48
+ const masked = maskFences(text);
49
+ const found = [];
50
+ for (const match of masked.matchAll(TASK)) {
51
+ const at = match.index ?? 0;
52
+ found.push(at + match[0].length - 3);
53
+ }
54
+ return found;
55
+ }
56
+ /** How many task items start before this offset: the ordinal of that item. */
57
+ export function ordinalAt(text, offset) {
58
+ return taskBoxes(text).filter((box) => box < offset).length;
59
+ }
60
+ /**
61
+ * Flips the n-th task box. Returns null when there is no such item, which is
62
+ * what a stale render looks like, and null is the honest answer: writing the
63
+ * wrong box would be worse than not writing.
64
+ */
65
+ export function toggleTaskAt(raw, ordinal) {
66
+ const boxes = taskBoxes(raw);
67
+ const at = boxes[ordinal];
68
+ if (at === undefined)
69
+ return null;
70
+ // `[x]` lowercase on the way in. GFM reads both cases, and picking one keeps
71
+ // the diff of a revision about what changed, not about how it was typed.
72
+ const current = raw[at + 1];
73
+ const next = current === ' ' ? 'x' : ' ';
74
+ return `${raw.slice(0, at + 1)}${next}${raw.slice(at + 2)}`;
75
+ }
76
+ /** Whether the n-th box is checked, for the optimistic state on the screen. */
77
+ export function taskCheckedAt(raw, ordinal) {
78
+ const at = taskBoxes(raw)[ordinal];
79
+ return at !== undefined && raw[at + 1] !== ' ';
80
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Transclusion: `![[note]]` and `![[note#section]]`, expanded for reading.
3
+ *
4
+ * The expansion belongs to the reading surface. It is the same class of work
5
+ * the wikilink resolution already does here: interpreting the notation to
6
+ * display it. Nothing changes in what the host stores: the body of the
7
+ * embedded note comes from the host, and the package never fetches.
8
+ *
9
+ * ONE LEVEL, AND ONLY ONE. An embed found inside transcluded content is drawn
10
+ * as a link to its target, which is what keeps a pair of notes that embed each
11
+ * other from hanging the page.
12
+ *
13
+ * **Three places an embed cannot expand, and one answer for all of them.**
14
+ * Inside embedded content, past the ceiling this file sets, and — since
15
+ * specification v0.4.0 named it — inside a table cell, because a cell holds inlines
16
+ * and never blocks (§4.1). Wherever expansion cannot happen the embed becomes
17
+ * a link and is never dropped, which is what `demoteEmbeds` does.
18
+ */
19
+ export type BodySegment = {
20
+ kind: 'text';
21
+ text: string;
22
+ } | {
23
+ kind: 'embed';
24
+ target: string;
25
+ anchor: string | null;
26
+ };
27
+ /**
28
+ * How many embeds one page expands. Past it the rest stay as links: the cost
29
+ * of a page has to stay predictable, and a note with forty embeds is a note
30
+ * that wanted a different structure.
31
+ */
32
+ export declare const EMBED_LIMIT = 10;
33
+ /**
34
+ * Splits a body into what to render inline and what to transclude. Embeds past
35
+ * the ceiling are demoted to plain wikilinks instead of being dropped, because
36
+ * losing the reference would be worse than not expanding it.
37
+ */
38
+ export declare function splitEmbeds(body: string,
39
+ /**
40
+ * Whether the notebook keeps a FILE under that name. An embed of one
41
+ * expands nothing here: it stays in the text run, where the wikilink pass
42
+ * turns it into the attachment the page draws by its type.
43
+ */
44
+ keeps?: (name: string) => boolean, limit?: number): BodySegment[];
45
+ /**
46
+ * `![[x]]` becomes `[[x]]`: a reference instead of an expansion.
47
+ *
48
+ * **An embed of a file keeps its `!`**. Demoting exists for an embed
49
+ * that cannot be expanded here — one inside a table cell, one inside embedded
50
+ * content — and an attachment is not one of those: the page draws it. Taking
51
+ * the bang off it was what made `[[picture]]` and `![[picture]]` arrive at the
52
+ * wikilink pass as the same three characters, so a **link** to a file drew the
53
+ * file. A link addresses a note (§5.2), and only the embed reaches a file.
54
+ */
55
+ export declare function demoteEmbeds(body: string, keeps?: (name: string) => boolean): string;
56
+ /**
57
+ * The block a `^identifier` names, or null when nothing names it (§7.7).
58
+ *
59
+ * A block is what the parser would call one: a run of lines up to a blank
60
+ * line. The identifier sits at the END of it, and the marker is left in the
61
+ * text on purpose — `remarkBlockIds` takes it off the page, and cutting it
62
+ * here would mean this function decides what the reader shows.
63
+ *
64
+ * An identifier that names nothing answers null, and the caller renders it the
65
+ * way it renders a pending link. It is never an error: a notebook is read most
66
+ * while it is being written.
67
+ */
68
+ export declare function blockOf(markdown: string, identifier: string): string | null;
69
+ /** An anchor written `^id` addresses a block; anything else, a section. */
70
+ export declare function isBlockAnchor(anchor: string): boolean;
71
+ /**
72
+ * The section of a document, cut syntactically: from the heading whose slug
73
+ * matches the anchor to the next heading of equal or higher level. No notebook
74
+ * convention takes part in this: the cut is syntactic.
75
+ */
76
+ export declare function sectionOf(markdown: string, anchor: string): string | null;