@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
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
import { codeRegions, headingKey, insideCode, insideTable, outsideCode, tableRegions } from './markdown.js';
|
|
20
|
+
/** Same shape the link extractor matches, plus the leading `!`. */
|
|
21
|
+
const EMBED = /!\[\[([^\]|#]+?)(?:#([^\]|]+?))?(?:\|[^\]]*?)?\]\]/g;
|
|
22
|
+
/**
|
|
23
|
+
* How many embeds one page expands. Past it the rest stay as links: the cost
|
|
24
|
+
* of a page has to stay predictable, and a note with forty embeds is a note
|
|
25
|
+
* that wanted a different structure.
|
|
26
|
+
*/
|
|
27
|
+
export const EMBED_LIMIT = 10;
|
|
28
|
+
/**
|
|
29
|
+
* Splits a body into what to render inline and what to transclude. Embeds past
|
|
30
|
+
* the ceiling are demoted to plain wikilinks instead of being dropped, because
|
|
31
|
+
* losing the reference would be worse than not expanding it.
|
|
32
|
+
*/
|
|
33
|
+
export function splitEmbeds(body,
|
|
34
|
+
/**
|
|
35
|
+
* Whether the notebook keeps a FILE under that name. An embed of one
|
|
36
|
+
* expands nothing here: it stays in the text run, where the wikilink pass
|
|
37
|
+
* turns it into the attachment the page draws by its type.
|
|
38
|
+
*/
|
|
39
|
+
keeps = () => false, limit = EMBED_LIMIT) {
|
|
40
|
+
const segments = [];
|
|
41
|
+
const code = codeRegions(body);
|
|
42
|
+
const tables = tableRegions(body);
|
|
43
|
+
let cursor = 0;
|
|
44
|
+
let expanded = 0;
|
|
45
|
+
for (const match of body.matchAll(EMBED)) {
|
|
46
|
+
const at = match.index ?? 0;
|
|
47
|
+
const target = (match[1] ?? '').trim();
|
|
48
|
+
if (!target)
|
|
49
|
+
continue;
|
|
50
|
+
// A file is not a note, so there is nothing to transclude: it is left in
|
|
51
|
+
// the text run, where the wikilink pass turns it into the attachment the
|
|
52
|
+
// page draws by its type.
|
|
53
|
+
if (keeps(target))
|
|
54
|
+
continue;
|
|
55
|
+
// An embed written inside code is an example of the notation, not a use of
|
|
56
|
+
// it: expanding it would replace the very text somebody was showing.
|
|
57
|
+
if (insideCode(code, at))
|
|
58
|
+
continue;
|
|
59
|
+
// An embed inside a table cell is drawn as a link (§7.3): no block
|
|
60
|
+
// fits in a cell. Skipping it here leaves it in the surrounding text run,
|
|
61
|
+
// where `demoteEmbeds` turns it into the wikilink the specification asks for —
|
|
62
|
+
// and, just as importantly, keeps the run from being cut mid-row, which
|
|
63
|
+
// used to empty the cell and leave the closing pipe as a paragraph.
|
|
64
|
+
if (insideTable(tables, at))
|
|
65
|
+
continue;
|
|
66
|
+
if (expanded >= limit)
|
|
67
|
+
break;
|
|
68
|
+
// Demoted like the tail is, and for the embeds this loop SKIPPED: one in
|
|
69
|
+
// a table cell lands in a run before an expanded one, and without this it
|
|
70
|
+
// would reach the page as the literal `![[…]]` — dropped notation, which
|
|
71
|
+
// is the one thing §7.3 forbids in every place expansion cannot happen.
|
|
72
|
+
// Code is untouched, because `demoteEmbeds` rewrites outside it.
|
|
73
|
+
const before = demoteEmbeds(body.slice(cursor, at), keeps);
|
|
74
|
+
if (before.length > 0)
|
|
75
|
+
segments.push({ kind: 'text', text: before });
|
|
76
|
+
segments.push({ kind: 'embed', target, anchor: match[2]?.trim() ?? null });
|
|
77
|
+
cursor = at + match[0].length;
|
|
78
|
+
expanded += 1;
|
|
79
|
+
}
|
|
80
|
+
const rest = body.slice(cursor);
|
|
81
|
+
if (rest.length > 0)
|
|
82
|
+
segments.push({ kind: 'text', text: demoteEmbeds(rest, keeps) });
|
|
83
|
+
return segments;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `![[x]]` becomes `[[x]]`: a reference instead of an expansion.
|
|
87
|
+
*
|
|
88
|
+
* **An embed of a file keeps its `!`**. Demoting exists for an embed
|
|
89
|
+
* that cannot be expanded here — one inside a table cell, one inside embedded
|
|
90
|
+
* content — and an attachment is not one of those: the page draws it. Taking
|
|
91
|
+
* the bang off it was what made `[[picture]]` and `![[picture]]` arrive at the
|
|
92
|
+
* wikilink pass as the same three characters, so a **link** to a file drew the
|
|
93
|
+
* file. A link addresses a note (§5.2), and only the embed reaches a file.
|
|
94
|
+
*/
|
|
95
|
+
export function demoteEmbeds(body, keeps = () => false) {
|
|
96
|
+
return outsideCode(body, (text) => text.replace(EMBED, (all, target) => (keeps(target.trim()) ? all : all.slice(1))));
|
|
97
|
+
}
|
|
98
|
+
const HEADING = /^(#{1,6})\s+(.+?)\s*$/;
|
|
99
|
+
/**
|
|
100
|
+
* The block a `^identifier` names, or null when nothing names it (§7.7).
|
|
101
|
+
*
|
|
102
|
+
* A block is what the parser would call one: a run of lines up to a blank
|
|
103
|
+
* line. The identifier sits at the END of it, and the marker is left in the
|
|
104
|
+
* text on purpose — `remarkBlockIds` takes it off the page, and cutting it
|
|
105
|
+
* here would mean this function decides what the reader shows.
|
|
106
|
+
*
|
|
107
|
+
* An identifier that names nothing answers null, and the caller renders it the
|
|
108
|
+
* way it renders a pending link. It is never an error: a notebook is read most
|
|
109
|
+
* while it is being written.
|
|
110
|
+
*/
|
|
111
|
+
export function blockOf(markdown, identifier) {
|
|
112
|
+
const marker = new RegExp(`[ \\t]\\^${escapeRegExp(identifier)}[ \\t]*$`);
|
|
113
|
+
const lines = markdown.split('\n');
|
|
114
|
+
for (let index = 0; index < lines.length; index++) {
|
|
115
|
+
if (!marker.test(lines[index] ?? ''))
|
|
116
|
+
continue;
|
|
117
|
+
// Walk back to the top of the block: the first line after a blank one.
|
|
118
|
+
let start = index;
|
|
119
|
+
while (start > 0 && (lines[start - 1] ?? '').trim().length > 0)
|
|
120
|
+
start--;
|
|
121
|
+
return lines
|
|
122
|
+
.slice(start, index + 1)
|
|
123
|
+
.join('\n')
|
|
124
|
+
.trimEnd();
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/** An anchor written `^id` addresses a block; anything else, a section. */
|
|
129
|
+
export function isBlockAnchor(anchor) {
|
|
130
|
+
return anchor.startsWith('^');
|
|
131
|
+
}
|
|
132
|
+
function escapeRegExp(value) {
|
|
133
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The section of a document, cut syntactically: from the heading whose slug
|
|
137
|
+
* matches the anchor to the next heading of equal or higher level. No notebook
|
|
138
|
+
* convention takes part in this: the cut is syntactic.
|
|
139
|
+
*/
|
|
140
|
+
export function sectionOf(markdown, anchor) {
|
|
141
|
+
const wanted = headingKey(anchor);
|
|
142
|
+
const lines = markdown.split('\n');
|
|
143
|
+
let start = -1;
|
|
144
|
+
let level = 0;
|
|
145
|
+
for (let index = 0; index < lines.length; index++) {
|
|
146
|
+
const heading = HEADING.exec(lines[index] ?? '');
|
|
147
|
+
if (!heading)
|
|
148
|
+
continue;
|
|
149
|
+
if (start === -1) {
|
|
150
|
+
if (headingKey(heading[2] ?? '') === wanted) {
|
|
151
|
+
start = index;
|
|
152
|
+
level = (heading[1] ?? '').length;
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if ((heading[1] ?? '').length <= level) {
|
|
157
|
+
return lines.slice(start, index).join('\n').trimEnd();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return start === -1 ? null : lines.slice(start).join('\n').trimEnd();
|
|
161
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResolvedHost } from './host.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where a wikilink goes: the note when exactly one answers it, and the choice
|
|
4
|
+
* otherwise — several notes, or none, which is pending and still asks — when
|
|
5
|
+
* the host can say which notes a target may lead to. With no such host, a
|
|
6
|
+
* target no single note answers is pending.
|
|
7
|
+
*/
|
|
8
|
+
export declare function wikilinkAddress(host: ResolvedHost, name: string): string | null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CHOICE_SCHEME } from './LinkChoice.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where a wikilink goes: the note when exactly one answers it, and the choice
|
|
4
|
+
* otherwise — several notes, or none, which is pending and still asks — when
|
|
5
|
+
* the host can say which notes a target may lead to. With no such host, a
|
|
6
|
+
* target no single note answers is pending.
|
|
7
|
+
*/
|
|
8
|
+
export function wikilinkAddress(host, name) {
|
|
9
|
+
const address = host.resolve(name);
|
|
10
|
+
if (address !== null)
|
|
11
|
+
return address;
|
|
12
|
+
return host.choose ? `${CHOICE_SCHEME}${encodeURIComponent(name)}` : null;
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memorysmithapp/markdown-renderer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A React component that draws a MemorySmith note exactly as MemorySmith's reading page does.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"markdown",
|
|
7
|
+
"react",
|
|
8
|
+
"remark",
|
|
9
|
+
"renderer",
|
|
10
|
+
"wikilink",
|
|
11
|
+
"callout",
|
|
12
|
+
"katex",
|
|
13
|
+
"mermaid",
|
|
14
|
+
"memorysmith"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/memorysmithapp/memorysmithapp-markdown/tree/main/packages/markdown-renderer#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/memorysmithapp/memorysmithapp-markdown/issues"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/memorysmithapp/memorysmithapp-markdown.git",
|
|
24
|
+
"directory": "packages/markdown-renderer"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": [
|
|
28
|
+
"*.css"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"dist/*.js",
|
|
32
|
+
"dist/*.d.ts",
|
|
33
|
+
"styles.css",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"default": "./dist/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./styles.css": "./styles.css"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"katex": "^0.18.5",
|
|
47
|
+
"react": "^19.0.0",
|
|
48
|
+
"react-dom": "^19.0.0"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"hast-util-to-jsx-runtime": "2.3.6",
|
|
52
|
+
"mermaid": "11.17.0",
|
|
53
|
+
"react-markdown": "9.1.0",
|
|
54
|
+
"refractor": "5.0.0",
|
|
55
|
+
"rehype-katex": "7.0.1",
|
|
56
|
+
"remark-gfm": "4.0.1",
|
|
57
|
+
"remark-math": "6.0.0",
|
|
58
|
+
"remark-parse": "11.0.0",
|
|
59
|
+
"unified": "11.0.5"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@memorysmithapp/markdown-spec": "0.1.1",
|
|
63
|
+
"@types/hast": "3.0.5",
|
|
64
|
+
"@types/react": "19.2.18",
|
|
65
|
+
"@types/react-dom": "19.2.4",
|
|
66
|
+
"katex": "0.18.5",
|
|
67
|
+
"react": "19.2.8",
|
|
68
|
+
"react-dom": "19.2.8"
|
|
69
|
+
},
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "tsc -p tsconfig.build.json",
|
|
75
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
76
|
+
"test": "cd ../.. && vitest run --project markdown-renderer"
|
|
77
|
+
}
|
|
6
78
|
}
|