@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,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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }