@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
package/dist/host.js ADDED
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from 'react';
3
+ import { withDefaults } from './strings.js';
4
+ /**
5
+ * Every member of the contract, by name. The `satisfies` is what makes it complete: a member added
6
+ * to `NoteHost` and not listed here does not compile.
7
+ */
8
+ export const HOST_KEYS = Object.keys({
9
+ resolve: 1,
10
+ reaches: 1,
11
+ choose: 1,
12
+ targetAddress: 1,
13
+ navigate: 1,
14
+ loadNote: 1,
15
+ writable: 1,
16
+ onToggleTask: 1,
17
+ keeps: 1,
18
+ renderAttachment: 1,
19
+ renderLink: 1,
20
+ strings: 1,
21
+ theme: 1,
22
+ remoteNotice: 1,
23
+ });
24
+ const nothing = () => null;
25
+ const never = () => false;
26
+ const plainLink = ({ href, className, children }) => (_jsx("a", { className: className, href: href, children: children }));
27
+ export function resolveHost(host) {
28
+ const resolve = host.resolve ?? nothing;
29
+ return {
30
+ resolve,
31
+ reaches: host.reaches ?? ((name) => (resolve(name) === null ? 0 : 1)),
32
+ choose: host.choose ?? null,
33
+ targetAddress: host.targetAddress ?? nothing,
34
+ navigate: host.navigate ?? ((address) => window.location.assign(address)),
35
+ remoteNotice: host.remoteNotice ?? true,
36
+ loadNote: host.loadNote ?? null,
37
+ keeps: host.keeps ?? never,
38
+ renderAttachment: host.renderAttachment ?? null,
39
+ renderLink: host.renderLink ?? plainLink,
40
+ strings: withDefaults(host.strings),
41
+ theme: host.theme ?? 'light',
42
+ };
43
+ }
44
+ const HostContext = createContext(resolveHost({}));
45
+ export const HostProvider = HostContext.Provider;
46
+ /** The host the note is drawn under, read by every part of the surface. */
47
+ export function useHost() {
48
+ return useContext(HostContext);
49
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The pipe inside the alt text of an image, which separates the description
3
+ * from the dimensions (`SPEC.md` §3.14).
4
+ *
5
+ * `![Engelbart|100x145](engelbart.jpg)` is 100 by 145 CSS pixels and
6
+ * `![Engelbart|100](…)` is 100 wide with the aspect ratio kept. What precedes
7
+ * the pipe is the **description** and is never dropped — it is what a person
8
+ * gets when the image does not load, and it is what a screen reader says.
9
+ *
10
+ * **A value that is neither form is not a dimension.** `![Engelbart|large](…)`
11
+ * keeps `Engelbart|large` as the whole description, because deleting what an
12
+ * author wrote into an accessibility label is the worse of the two failures.
13
+ * Until the specification declared this form, an imported note displayed the
14
+ * pipe and the digits on screen and mandated the whole string as the label.
15
+ */
16
+ export interface ImageAlt {
17
+ /** What a reader hears, and sees when the image does not load. */
18
+ readonly description: string;
19
+ readonly width: number | null;
20
+ readonly height: number | null;
21
+ }
22
+ /**
23
+ * What follows a pipe, when it is a dimension and not text.
24
+ *
25
+ * The wikilink embed of an attachment carries the same thing the alt text of a
26
+ * Markdown image does — `![[engelbart.jpg|100x145]]` and
27
+ * `![Engelbart|100x145](…)` mean one thing — so both read it here.
28
+ */
29
+ export declare function readDimensions(value: string): {
30
+ width: number;
31
+ height: number | null;
32
+ } | null;
33
+ export declare function readImageAlt(alt: string): ImageAlt;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The pipe inside the alt text of an image, which separates the description
3
+ * from the dimensions (`SPEC.md` §3.14).
4
+ *
5
+ * `![Engelbart|100x145](engelbart.jpg)` is 100 by 145 CSS pixels and
6
+ * `![Engelbart|100](…)` is 100 wide with the aspect ratio kept. What precedes
7
+ * the pipe is the **description** and is never dropped — it is what a person
8
+ * gets when the image does not load, and it is what a screen reader says.
9
+ *
10
+ * **A value that is neither form is not a dimension.** `![Engelbart|large](…)`
11
+ * keeps `Engelbart|large` as the whole description, because deleting what an
12
+ * author wrote into an accessibility label is the worse of the two failures.
13
+ * Until the specification declared this form, an imported note displayed the
14
+ * pipe and the digits on screen and mandated the whole string as the label.
15
+ */
16
+ /** `100` or `100x145`, and nothing else. */
17
+ const DIMENSIONS = /^(\d{1,5})(?:x(\d{1,5}))?$/;
18
+ /**
19
+ * What follows a pipe, when it is a dimension and not text.
20
+ *
21
+ * The wikilink embed of an attachment carries the same thing the alt text of a
22
+ * Markdown image does — `![[engelbart.jpg|100x145]]` and
23
+ * `![Engelbart|100x145](…)` mean one thing — so both read it here.
24
+ */
25
+ export function readDimensions(value) {
26
+ const measured = DIMENSIONS.exec(value.trim());
27
+ if (!measured)
28
+ return null;
29
+ return {
30
+ width: Number(measured[1]),
31
+ height: measured[2] === undefined ? null : Number(measured[2]),
32
+ };
33
+ }
34
+ export function readImageAlt(alt) {
35
+ const at = alt.lastIndexOf('|');
36
+ if (at === -1)
37
+ return { description: alt, width: null, height: null };
38
+ const measured = readDimensions(alt.slice(at + 1));
39
+ if (!measured)
40
+ return { description: alt, width: null, height: null };
41
+ return { description: alt.slice(0, at).trim(), width: measured.width, height: measured.height };
42
+ }
@@ -0,0 +1,8 @@
1
+ export { NoteBody, type NoteBodyProps } from './NoteBody.js';
2
+ export type { AttachmentRequest, EmbeddedNote, LinkChoiceNote, LinkChoices, LinkRequest, NoteHost } from './host.js';
3
+ export { ENGLISH, type NoteStrings } from './strings.js';
4
+ export { followable } from './address.js';
5
+ export { readDimensions, readImageAlt, type ImageAlt } from './image-dimensions.js';
6
+ export { codeRegions, fenceRegions, headingKey, remoteImageHosts, resolveWikilinks, splitFrontmatter, tableRegions, toUnixNewlines, type SplitDocument, } from './markdown.js';
7
+ export { ordinalAt, taskBoxes, taskCheckedAt, toggleTaskAt } from './tasklist.js';
8
+ export { DELEGATED_TO_THE_BASE_PARSER, unclassified } from './delegated.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // The entry point of @memorysmithapp/markdown-renderer.
2
+ export { NoteBody } from './NoteBody.js';
3
+ export { ENGLISH } from './strings.js';
4
+ export { followable } from './address.js';
5
+ export { readDimensions, readImageAlt } from './image-dimensions.js';
6
+ export { codeRegions, fenceRegions, headingKey, remoteImageHosts, resolveWikilinks, splitFrontmatter, tableRegions, toUnixNewlines, } from './markdown.js';
7
+ export { ordinalAt, taskBoxes, taskCheckedAt, toggleTaskAt } from './tasklist.js';
8
+ export { DELEGATED_TO_THE_BASE_PARSER, unclassified } from './delegated.js';
@@ -0,0 +1,68 @@
1
+ export interface SplitDocument {
2
+ frontmatter: Record<string, string>;
3
+ /**
4
+ * Which of those keys the notebook wrote as a list, whether in a dash block or
5
+ * inline. The values are flattened to one comma-separated string so the
6
+ * table can print them, and that flattening is lossy: `tags: a, b` and a
7
+ * two-item list read the same afterwards. The set keeps the difference,
8
+ * because a list is drawn as chips and a sentence is not, and guessing from
9
+ * the commas would turn a prose value that happens to have one into chips.
10
+ */
11
+ lists: Set<string>;
12
+ body: string;
13
+ }
14
+ /**
15
+ * A note is bytes the notebook wrote, and a notebook written on Windows, or in a
16
+ * desktop editor, or pasted from anywhere, carries CRLF. Every reader in this
17
+ * module starts here, because a carriage return left at the end of a line is
18
+ * invisible in the source and fatal to a regex: `.` does not match one, so
19
+ * `(.*)$` fails on exactly the lines that have a value.
20
+ */
21
+ export declare function toUnixNewlines(raw: string): string;
22
+ /**
23
+ * Where the body of a note starts: after its frontmatter, which a host shows as properties and
24
+ * not as prose. The cut is the one MemorySmith's reading page makes, so the ordinals of the task
25
+ * boxes below it are counted over the whole note, as the host stores it.
26
+ */
27
+ export declare function bodyStart(text: string): number;
28
+ /**
29
+ * The frontmatter block, read the way MemorySmith's reading page reads it to show a note's
30
+ * properties. The package does not draw it — `NoteBody` starts after it — and exports this so a
31
+ * host that draws the properties reads the same values MemorySmith does.
32
+ */
33
+ export declare function splitFrontmatter(input: string): SplitDocument;
34
+ /** The half-open ranges of `body` that are fenced blocks. */
35
+ export declare function fenceRegions(body: string): Array<[number, number]>;
36
+ /** The half-open ranges of `body` that are code, fenced or inline, in order. */
37
+ export declare function codeRegions(body: string): Array<[number, number]>;
38
+ /** Whether an offset falls inside one of them. */
39
+ export declare function insideCode(regions: Array<[number, number]>, at: number): boolean;
40
+ export declare function tableRegions(body: string): Array<[number, number]>;
41
+ /** Whether an offset falls inside one of them. */
42
+ export declare function insideTable(regions: Array<[number, number]>, at: number): boolean;
43
+ /** Runs a rewrite over everything except code, which is copied through. */
44
+ export declare function outsideCode(body: string, rewrite: (text: string) => string): string;
45
+ export declare function resolveWikilinks(body: string, resolve: (name: string) => string | null,
46
+ /**
47
+ * Whether the notebook keeps a FILE under that name. The extension of a
48
+ * name decides nothing, so what a target is cannot be read off it: it is
49
+ * read off what the notebook holds, which is the host's to answer.
50
+ */
51
+ keeps?: (name: string) => boolean): string;
52
+ /**
53
+ * The key a **heading** is matched by, which is how an anchor finds the
54
+ * section it names inside a note.
55
+ *
56
+ * **It is not the key of a note, and there is no longer one of those.** A link
57
+ * resolves against the name, literally and case-exact (§5.2), so the
58
+ * slug this file used to compute — the second implementation of a rule the
59
+ * kernel also implemented, and the one that drifted — is gone. What is
60
+ * left is this: an anchor and a heading are two spellings of the same phrase,
61
+ * written by the same person minutes apart, and folding case and punctuation
62
+ * between them is what makes `#Artigo 75` find `## Artigo 75`.
63
+ *
64
+ * Nothing outside this comparison reads it, and nothing else in the renderer
65
+ * computes a key from a name: the address of a note is the host's to give.
66
+ */
67
+ export declare function headingKey(name: string): string;
68
+ export declare function remoteImageHosts(body: string): string[];
@@ -0,0 +1,395 @@
1
+ // The text helpers of the reading surface: what a note becomes before the parser reads it.
2
+ // Nothing here rewrites the note a host keeps; each function returns a new string for display.
3
+ import { readDimensions } from './image-dimensions.js';
4
+ /**
5
+ * A note is bytes the notebook wrote, and a notebook written on Windows, or in a
6
+ * desktop editor, or pasted from anywhere, carries CRLF. Every reader in this
7
+ * module starts here, because a carriage return left at the end of a line is
8
+ * invisible in the source and fatal to a regex: `.` does not match one, so
9
+ * `(.*)$` fails on exactly the lines that have a value.
10
+ */
11
+ export function toUnixNewlines(raw) {
12
+ return raw.replace(/\r\n?/g, '\n');
13
+ }
14
+ /**
15
+ * Where the body of a note starts: after its frontmatter, which a host shows as properties and
16
+ * not as prose. The cut is the one MemorySmith's reading page makes, so the ordinals of the task
17
+ * boxes below it are counted over the whole note, as the host stores it.
18
+ */
19
+ export function bodyStart(text) {
20
+ return text.startsWith('---') ? text.indexOf('\n---', 3) + 4 : 0;
21
+ }
22
+ /**
23
+ * Strips one layer of matching quotes, as the facet projector does. The trim
24
+ * comes first: an item of an inline list arrives with the space that followed
25
+ * the comma, and a quote that is not at position zero is not stripped.
26
+ */
27
+ function unquote(value) {
28
+ return value
29
+ .trim()
30
+ .replace(/^["']|["']$/g, '')
31
+ .trim();
32
+ }
33
+ /**
34
+ * The frontmatter block, read the way MemorySmith's reading page reads it to show a note's
35
+ * properties. The package does not draw it — `NoteBody` starts after it — and exports this so a
36
+ * host that draws the properties reads the same values MemorySmith does.
37
+ */
38
+ export function splitFrontmatter(input) {
39
+ const raw = toUnixNewlines(input);
40
+ const bare = { frontmatter: {}, lists: new Set(), body: raw };
41
+ if (!raw.startsWith('---'))
42
+ return bare;
43
+ const end = raw.indexOf('\n---', 3);
44
+ if (end === -1)
45
+ return bare;
46
+ const head = raw.slice(raw.indexOf('\n') + 1, end);
47
+ const body = raw.slice(raw.indexOf('\n', end + 1) + 1);
48
+ const frontmatter = {};
49
+ const lists = new Set();
50
+ let lastKey = null;
51
+ for (const line of head.split('\n')) {
52
+ const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
53
+ if (kv && kv[1]) {
54
+ lastKey = kv[1];
55
+ const value = (kv[2] ?? '').trim();
56
+ // An inline list becomes the same comma-separated string a dash list
57
+ // produces, so `tags: [a, b]` and a `tags:` block read alike from here.
58
+ const inlineList = value.startsWith('[') && value.endsWith(']');
59
+ if (inlineList)
60
+ lists.add(lastKey);
61
+ frontmatter[lastKey] = inlineList
62
+ ? value
63
+ .slice(1, -1)
64
+ .split(',')
65
+ .map(unquote)
66
+ .filter((item) => item.length > 0)
67
+ .join(', ')
68
+ : unquote(value);
69
+ }
70
+ else if (lastKey && /^\s+-\s+/.test(line)) {
71
+ const item = unquote(line.replace(/^\s+-\s+/, ''));
72
+ lists.add(lastKey);
73
+ frontmatter[lastKey] = frontmatter[lastKey] ? `${frontmatter[lastKey]}, ${item}` : item;
74
+ }
75
+ }
76
+ return { frontmatter, lists, body };
77
+ }
78
+ /**
79
+ * A wikilink, with the `!` in front of it when it is an embed.
80
+ *
81
+ * The bang is captured and not assumed away, because it is what tells a
82
+ * **link** from an **embed**, and the two reach different things: a link
83
+ * addresses a note and nothing else (§5.2), while an embed may also name a
84
+ * file the notebook keeps (§5.8). By the time this pass runs, the bang of a
85
+ * note embed has been taken off by `demoteEmbeds` — an embed that could not be
86
+ * expanded is a reference — so what still carries one is an embed of a file.
87
+ */
88
+ const WIKILINK = /(!)?\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g;
89
+ /**
90
+ * Code, where notation is characters and nothing else.
91
+ *
92
+ * A fenced block parses nothing inside it and a code span is where an author
93
+ * writes notation without invoking it — both stated by the specification, in the
94
+ * `code-fenced` and `code-span` entries of `spec.json`. The rewrites below
95
+ * run over the raw text BEFORE the parser sees it, so they are the one place
96
+ * in the renderer that has to know that on its own: every other reading of
97
+ * the notation is a remark plugin, working on a tree where a code node is
98
+ * already a code node.
99
+ *
100
+ * Code is found by the rules the specification states and never by a pattern of
101
+ * backticks: a fence opens on a line whose first thing is a run of
102
+ * three or more backticks or tildes, closes on a line holding a run of the
103
+ * same character, at least as long, and nothing else, and otherwise runs to
104
+ * the end of the note; a code span closes on a run of exactly its own length.
105
+ * A pattern pairing any two runs of three backticks read a span SHOWING three
106
+ * backticks as an opening fence, and every block after it out of phase. The
107
+ * link extractor of MemorySmith reads code by the same rules, and both are held
108
+ * to the `link-in-code` cases of the conformance suite.
109
+ *
110
+ * The indented form is deliberately NOT here, and the reason is the cost of
111
+ * being wrong. Telling four spaces of code from four spaces of a nested list
112
+ * item needs the block context only a parser has; guessing it wrong turns a
113
+ * real link into text, and the same guess in the link extractor would drop a
114
+ * real edge. A wikilink written inside an indented code block is therefore
115
+ * still read, in both readers, and that is declared rather than discovered
116
+ * in MemorySmith's architecture. For the same reason a fence is accepted
117
+ * after whatever its list item or block quote puts in front of it, and a code
118
+ * span is not followed across a line break.
119
+ */
120
+ const CONTAINER = String.raw `(?:[ \t]*(?:>[ \t]?|(?:[-*+]|\d{1,9}[.)])[ \t]+))*[ \t]*`;
121
+ const OPENING_FENCE = new RegExp(`^(${CONTAINER})(\`{3,}|~{3,})(.*?)\r?$`);
122
+ const CLOSING_FENCE = new RegExp(`^${CONTAINER}(\`{3,}|~{3,})[ \t]*\r?$`);
123
+ /**
124
+ * The fenced blocks of `body`, as half-open ranges from the opening run to the
125
+ * end of the closing line, and the lines no fence holds, which are where code
126
+ * spans may be.
127
+ */
128
+ function scanFences(body) {
129
+ const fences = [];
130
+ const prose = [];
131
+ let open = null;
132
+ let at = 0;
133
+ for (const line of body.split('\n')) {
134
+ const end = at + line.length;
135
+ if (open) {
136
+ const run = CLOSING_FENCE.exec(line)?.[1];
137
+ if (run && run[0] === open.char && run.length >= open.length) {
138
+ fences.push([open.start, end]);
139
+ open = null;
140
+ }
141
+ }
142
+ else {
143
+ const fence = OPENING_FENCE.exec(line);
144
+ const run = fence?.[2] ?? '';
145
+ // A backtick fence may not carry a backtick in its info string: such a
146
+ // line is a paragraph, and what looks like a fence in it is a span.
147
+ if (fence && !(run[0] === '`' && (fence[3] ?? '').includes('`'))) {
148
+ open = { char: run[0] ?? '`', length: run.length, start: at + (fence[1] ?? '').length };
149
+ }
150
+ else {
151
+ prose.push({ line, at });
152
+ }
153
+ }
154
+ at = end + 1;
155
+ }
156
+ if (open)
157
+ fences.push([open.start, body.length]);
158
+ return { fences, prose };
159
+ }
160
+ /**
161
+ * The code spans of one line: a run of backticks closes on the next run of
162
+ * exactly its length, a run with no partner is text, and a backtick escaped by
163
+ * a backslash opens nothing.
164
+ */
165
+ function codeSpans(line, offset) {
166
+ const runs = [...line.matchAll(/`+/g)].map((match) => ({
167
+ index: match.index ?? 0,
168
+ length: match[0].length,
169
+ }));
170
+ const spans = [];
171
+ for (let i = 0; i < runs.length; i++) {
172
+ const run = runs[i];
173
+ if (!run)
174
+ continue;
175
+ const escaped = /(^|[^\\])(\\\\)*\\$/.test(line.slice(0, run.index));
176
+ const start = escaped ? run.index + 1 : run.index;
177
+ const length = escaped ? run.length - 1 : run.length;
178
+ if (length === 0)
179
+ continue;
180
+ const close = runs.findIndex((other, j) => j > i && other.length === length);
181
+ const closing = runs[close];
182
+ if (close === -1 || !closing)
183
+ continue;
184
+ spans.push([offset + start, offset + closing.index + closing.length]);
185
+ i = close;
186
+ }
187
+ return spans;
188
+ }
189
+ /** The half-open ranges of `body` that are fenced blocks. */
190
+ export function fenceRegions(body) {
191
+ return scanFences(body).fences;
192
+ }
193
+ /** The half-open ranges of `body` that are code, fenced or inline, in order. */
194
+ export function codeRegions(body) {
195
+ const { fences, prose } = scanFences(body);
196
+ const spans = prose.flatMap(({ line, at }) => codeSpans(line, at));
197
+ return [...fences, ...spans].sort((a, b) => a[0] - b[0]);
198
+ }
199
+ /** Whether an offset falls inside one of them. */
200
+ export function insideCode(regions, at) {
201
+ return regions.some(([start, end]) => at >= start && at < end);
202
+ }
203
+ /**
204
+ * A GFM table, which is the third place an embed cannot expand (§7.3).
205
+ *
206
+ * A cell holds inlines and never blocks (§4.1), so an embed of a note in a
207
+ * cell is drawn as a link, the same answer the one-level rule and the embed
208
+ * ceiling already give. What made this its own case is that the expansion is
209
+ * decided on the raw string, by cutting the body into runs around each
210
+ * `![[…]]`, and a cut inside a table row does not merely fail to expand: it
211
+ * ends the run mid-row, so the parser that reads it has an unterminated table
212
+ * and the closing pipe becomes a paragraph of its own.
213
+ *
214
+ * Detected here rather than in a plugin for the reason the code regions
215
+ * already are: the split happens before anything is parsed, so this is the one
216
+ * place in the renderer that has to recognise the block on its own.
217
+ *
218
+ * A table is a line containing a pipe, followed by a delimiter row of dashes,
219
+ * and it runs to the first blank line. The outer pipes are optional, which is
220
+ * why the delimiter row is what identifies the block. A shape this misses
221
+ * costs nothing new — it is the behaviour of before — so the pattern is
222
+ * deliberately the conservative one.
223
+ */
224
+ const DELIMITER_ROW = /^ {0,3}\|?[ \t]*:?-{1,}:?[ \t]*(\|[ \t]*:?-{1,}:?[ \t]*)*\|?[ \t]*$/;
225
+ export function tableRegions(body) {
226
+ const regions = [];
227
+ const code = codeRegions(body);
228
+ const lines = body.split('\n');
229
+ const offsets = [];
230
+ let at = 0;
231
+ for (const line of lines) {
232
+ offsets.push(at);
233
+ at += line.length + 1;
234
+ }
235
+ for (let index = 0; index + 1 < lines.length; index++) {
236
+ const header = lines[index] ?? '';
237
+ const delimiter = lines[index + 1] ?? '';
238
+ if (!header.includes('|') || !delimiter.includes('|'))
239
+ continue;
240
+ if (!DELIMITER_ROW.test(delimiter))
241
+ continue;
242
+ if (insideCode(code, offsets[index] ?? 0))
243
+ continue;
244
+ let last = index + 1;
245
+ while (last + 1 < lines.length && (lines[last + 1] ?? '').trim().length > 0)
246
+ last++;
247
+ regions.push([offsets[index] ?? 0, (offsets[last] ?? 0) + (lines[last] ?? '').length]);
248
+ index = last;
249
+ }
250
+ return regions;
251
+ }
252
+ /** Whether an offset falls inside one of them. */
253
+ export function insideTable(regions, at) {
254
+ return regions.some(([start, end]) => at >= start && at < end);
255
+ }
256
+ /** Runs a rewrite over everything except code, which is copied through. */
257
+ export function outsideCode(body, rewrite) {
258
+ let out = '';
259
+ let cursor = 0;
260
+ for (const [start, end] of codeRegions(body)) {
261
+ out += rewrite(body.slice(cursor, start)) + body.slice(start, end);
262
+ cursor = end;
263
+ }
264
+ return out + rewrite(body.slice(cursor));
265
+ }
266
+ // Replaces [[wikilinks]] with markdown links. Resolved targets point at the
267
+ // note route; unresolved ones become pending: links styled by the renderer.
268
+ export function resolveWikilinks(body, resolve,
269
+ /**
270
+ * Whether the notebook keeps a FILE under that name. The extension of a
271
+ * name decides nothing, so what a target is cannot be read off it: it is
272
+ * read off what the notebook holds, which is the host's to answer.
273
+ */
274
+ keeps = () => false) {
275
+ return outsideCode(body, (text) => resolveWikilinksIn(text, resolve, keeps));
276
+ }
277
+ /**
278
+ * The target of a wikilink is a NAME, and it is literal: nothing in it is
279
+ * decoded, no extension is removed and no path segment is discarded
280
+ * (§5.2). It used to be slugified here, which is what made a link differ
281
+ * from its note over an accent or a capital.
282
+ */
283
+ function resolveWikilinksIn(body, resolve, keeps) {
284
+ return body.replace(WIKILINK, (_all, bang, target, label) => {
285
+ const clean = target.split('#')[0]?.trim().replace(/\\$/, '').trim() ?? '';
286
+ const embed = bang !== undefined;
287
+ const text = displayText(clean, label, target, (name) => embed && keeps(name));
288
+ if (!clean)
289
+ return `[${text}](pending:)`;
290
+ // The pipe is read by WHAT THE TARGET IS: a note takes the alias, an
291
+ // attachment takes the dimensions, and a target that resolves to neither
292
+ // takes the alias — because reading it as a dimension would discard text
293
+ // an author wrote.
294
+ //
295
+ // **Only an embed reaches a file**. A `[[name]]` is resolved against
296
+ // the notes of the notebook and their aliases, and against nothing else:
297
+ // §5.2 lists the whole of resolution and an attachment is in none of its
298
+ // steps, so a name only a file carries is a pending link like any other.
299
+ if (embed && keeps(clean)) {
300
+ // The dimension travels with the address, because the name is already
301
+ // percent-encoded and a bare `|` after it cannot be part of it.
302
+ // Dropping it here is what made `![[picture|120]]` render at full size:
303
+ // the notation was read and then thrown away one line before it was used.
304
+ const measured = label === undefined ? null : readDimensions(label);
305
+ const sized = measured
306
+ ? `|${measured.width}${measured.height === null ? '' : `x${measured.height}`}`
307
+ : '';
308
+ return `[${text}](attachment:${encodeURIComponent(clean)}${sized})`;
309
+ }
310
+ const url = resolve(clean.normalize('NFC'));
311
+ return url ? `[${text}](${url})` : `[${text}](pending:${encodeURIComponent(clean)})`;
312
+ });
313
+ }
314
+ /**
315
+ * What the link shows. The pipe of an attachment carries dimensions and is
316
+ * never rendered as text, so what is left to show is the name of the file.
317
+ */
318
+ function displayText(clean, label, target, keeps) {
319
+ if (clean && keeps(clean))
320
+ return clean;
321
+ return (label ?? target).trim();
322
+ }
323
+ /** The longest a heading key may be, which is what an anchor is matched on. */
324
+ const MAX_KEY_LENGTH = 80;
325
+ /**
326
+ * The key a **heading** is matched by, which is how an anchor finds the
327
+ * section it names inside a note.
328
+ *
329
+ * **It is not the key of a note, and there is no longer one of those.** A link
330
+ * resolves against the name, literally and case-exact (§5.2), so the
331
+ * slug this file used to compute — the second implementation of a rule the
332
+ * kernel also implemented, and the one that drifted — is gone. What is
333
+ * left is this: an anchor and a heading are two spellings of the same phrase,
334
+ * written by the same person minutes apart, and folding case and punctuation
335
+ * between them is what makes `#Artigo 75` find `## Artigo 75`.
336
+ *
337
+ * Nothing outside this comparison reads it, and nothing else in the renderer
338
+ * computes a key from a name: the address of a note is the host's to give.
339
+ */
340
+ export function headingKey(name) {
341
+ return name
342
+ .normalize('NFD')
343
+ .replace(/(\d)[.,](\d)/g, '$1$2')
344
+ .replace(/[̀-ͯ]/g, '')
345
+ .toLowerCase()
346
+ .replace(/[^a-z0-9]+/g, '-')
347
+ .replace(/^-+|-+$/g, '')
348
+ .slice(0, MAX_KEY_LENGTH)
349
+ .replace(/-+$/g, '');
350
+ }
351
+ /**
352
+ * The hosts a note asks the reader's browser to talk to (§7.10).
353
+ *
354
+ * An image whose destination names a host is a **request to that host**, made
355
+ * when the note is opened, by whoever opens it — and the trigger was written
356
+ * by whoever wrote the note, which is the same sentence §7.9 uses about raw
357
+ * HTML. What travels with it is the reader's address, their user-agent and the
358
+ * moment they read it.
359
+ *
360
+ * The specification admits three answers and forbids only silence: never fetching,
361
+ * fetching on the reader's action, or fetching and saying so. MemorySmith,
362
+ * and this renderer with it, gives the third, so this is the half that says so. It is the image of §3.14
363
+ * and nothing else: a diagram and a formula render locally, and a link —
364
+ * bracketed, bare or autolinked — is a navigation the person initiates rather
365
+ * than a request the page makes.
366
+ *
367
+ * Code is excluded, because an image written inside a fence is an example of
368
+ * the notation and fetches nothing.
369
+ */
370
+ const IMAGE_DESTINATION = /!\[[^\]]*\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g;
371
+ export function remoteImageHosts(body) {
372
+ const regions = codeRegions(body);
373
+ const hosts = new Set();
374
+ for (const match of body.matchAll(IMAGE_DESTINATION)) {
375
+ if (insideCode(regions, match.index ?? 0))
376
+ continue;
377
+ const destination = (match[1] ?? '').trim();
378
+ // A path, a relative target or an anchor names no host and asks nobody
379
+ // for anything. Only a scheme or a leading `//` reaches outside.
380
+ const authority = /^[a-z][a-z0-9+.-]*:\/\//i.test(destination)
381
+ ? destination.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '')
382
+ : destination.startsWith('//')
383
+ ? destination.slice(2)
384
+ : '';
385
+ if (authority === '')
386
+ continue;
387
+ // The authority, minus any credentials, port and path: what a person needs
388
+ // in order to recognise who they just talked to.
389
+ const host = (authority.split('/')[0] ?? '').split('@').pop() ?? '';
390
+ const named = host.split(':')[0] ?? '';
391
+ if (named !== '')
392
+ hosts.add(named.toLowerCase());
393
+ }
394
+ return [...hosts];
395
+ }
@@ -0,0 +1,40 @@
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
+ interface MdastNode {
22
+ type: string;
23
+ value?: string;
24
+ children?: MdastNode[];
25
+ data?: {
26
+ hName?: string;
27
+ hProperties?: Record<string, unknown>;
28
+ };
29
+ }
30
+ export interface CalloutOptions {
31
+ /**
32
+ * The title of a callout written without one, in the language of whoever
33
+ * reads it. It answers `undefined` for a type it has no word for,
34
+ * and that type titles itself, capitalised, as the editors do: the
35
+ * vocabulary is open (§7.1) and a word nobody translated is still a word.
36
+ */
37
+ readonly titleOf?: (kind: string) => string | undefined;
38
+ }
39
+ export declare function remarkCallouts(options?: CalloutOptions): (tree: MdastNode) => void;
40
+ export {};