@memorysmithapp/markdown-renderer 0.1.2 → 0.3.0

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.
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Themes: how a note looks, written by whoever draws it.
3
+ *
4
+ * A theme decides how a note LOOKS, at three levels a developer can mix:
5
+ *
6
+ * 1. **Properties** — the values the stylesheet reads, by name. The renderer
7
+ * writes them on the root of the note as `--markdown-*` custom properties,
8
+ * so a theme that only sets values needs no CSS of its own.
9
+ * 2. **Its own CSS** — the root carries `markdown-theme-<name>`, so a theme's
10
+ * stylesheet reaches anything inside the note and nothing outside it.
11
+ * 3. **Components** — a closed set of slots (`ThemeComponents`), each given the
12
+ * notation already interpreted, which only draw it.
13
+ *
14
+ * What a notation MEANS is never a theme's: the parsing, the resolution of a
15
+ * wikilink, the address filter, the refusal of raw HTML, the ordinal a task box
16
+ * reports and the numbering of footnotes happen before a slot is called, and a
17
+ * slot is handed their result and no way around it.
18
+ *
19
+ * MemorySmith's look is the theme `default`: the package's stylesheet with no
20
+ * property set. It is the theme every other one starts from, since a property
21
+ * is read by the default theme's rules.
22
+ */
23
+ import type { ReactNode } from 'react';
24
+ import type { PropertyEntry } from './properties.js';
25
+ export type Scheme = 'light' | 'dark';
26
+ /**
27
+ * Every value the stylesheet reads, by name. Each becomes `--markdown-<name in
28
+ * kebab case>`; a value is any CSS the property takes. One left out keeps the
29
+ * default theme's value for the scheme being drawn.
30
+ */
31
+ export interface ThemeProperties {
32
+ readonly bg?: string;
33
+ readonly surface?: string;
34
+ readonly border?: string;
35
+ readonly text?: string;
36
+ readonly textSoft?: string;
37
+ readonly accent?: string;
38
+ readonly pending?: string;
39
+ readonly codeBg?: string;
40
+ readonly codeColor?: string;
41
+ readonly codeBorder?: string;
42
+ readonly inlineCodeBg?: string;
43
+ readonly inlineCodePadding?: string;
44
+ readonly tokenKeyword?: string;
45
+ readonly tokenString?: string;
46
+ readonly tokenNumber?: string;
47
+ readonly tokenComment?: string;
48
+ readonly tokenPunctuation?: string;
49
+ readonly calloutInfo?: string;
50
+ readonly calloutIdea?: string;
51
+ readonly calloutDone?: string;
52
+ readonly calloutWarn?: string;
53
+ readonly calloutStop?: string;
54
+ readonly calloutAside?: string;
55
+ readonly calloutQuote?: string;
56
+ readonly iconNote?: string;
57
+ readonly iconInfo?: string;
58
+ readonly iconSummary?: string;
59
+ readonly iconTip?: string;
60
+ readonly iconSuccess?: string;
61
+ readonly iconQuestion?: string;
62
+ readonly iconWarning?: string;
63
+ readonly iconFailure?: string;
64
+ readonly iconDanger?: string;
65
+ readonly iconExample?: string;
66
+ readonly iconQuote?: string;
67
+ readonly fontSans?: string;
68
+ readonly fontDisplay?: string;
69
+ readonly fontMono?: string;
70
+ readonly fontHeading?: string;
71
+ readonly radius?: string;
72
+ readonly radiusLarge?: string;
73
+ readonly fontSize?: string;
74
+ readonly lineHeight?: string;
75
+ readonly lineHeightTight?: string;
76
+ readonly rootLineHeight?: string;
77
+ readonly paragraphSpacing?: string;
78
+ readonly headingSpacing?: string;
79
+ readonly listIndent?: string;
80
+ readonly listSpacing?: string;
81
+ readonly headingWeight?: string;
82
+ readonly headingColor?: string;
83
+ readonly h1Size?: string;
84
+ readonly h2Size?: string;
85
+ readonly h3Size?: string;
86
+ readonly h4Size?: string;
87
+ readonly h5Size?: string;
88
+ readonly h6Size?: string;
89
+ readonly h1Weight?: string;
90
+ readonly h6Color?: string;
91
+ readonly quoteBorderColor?: string;
92
+ readonly quoteBorderWidth?: string;
93
+ readonly quoteColor?: string;
94
+ readonly tableBorder?: string;
95
+ readonly tableCellPadding?: string;
96
+ readonly tableHeaderBg?: string;
97
+ readonly tableHeaderRule?: string;
98
+ readonly tableStripeBg?: string;
99
+ readonly tableHoverBg?: string;
100
+ readonly embedRule?: string;
101
+ readonly titleFont?: string;
102
+ readonly titleSize?: string;
103
+ readonly titleWeight?: string;
104
+ readonly titleColor?: string;
105
+ readonly propertiesRule?: string;
106
+ readonly propertiesLabelFont?: string;
107
+ readonly propertiesLabelColor?: string;
108
+ readonly propertiesKeyColor?: string;
109
+ readonly iconPropertyText?: string;
110
+ readonly iconPropertyList?: string;
111
+ readonly iconPropertyDate?: string;
112
+ readonly iconPropertyBoolean?: string;
113
+ readonly chipBg?: string;
114
+ readonly chipColor?: string;
115
+ readonly chipFont?: string;
116
+ }
117
+ /** The names of `ThemeProperties`, in the order the documentation lists them. */
118
+ export declare const THEME_PROPERTIES: (keyof ThemeProperties)[];
119
+ /** `textSoft` → `--markdown-text-soft`, `h1Size` → `--markdown-h1-size`. */
120
+ export declare function customPropertyOf(name: keyof ThemeProperties): string;
121
+ /** A callout (§7.1), with its kind already read and its title already chosen. */
122
+ export interface CalloutSlotProps {
123
+ /** The kind as written, lower-cased: `warning`, `tip`, a kind nobody declared. */
124
+ readonly kind: string;
125
+ /** The fold marker written after the kind: `+`, `-`, or none. */
126
+ readonly fold: '+' | '-' | null;
127
+ /** The title: the author's, or the kind in the reader's words. */
128
+ readonly title: ReactNode;
129
+ /** The icon the theme gives this kind, or `null` when the stylesheet draws it. */
130
+ readonly icon: ReactNode;
131
+ /** The body. */
132
+ readonly children: ReactNode;
133
+ }
134
+ /** A table; each cell already carries its alignment. */
135
+ export interface TableSlotProps {
136
+ /** The alignment of each column, from the delimiter row. */
137
+ readonly align: readonly ('left' | 'center' | 'right' | null)[];
138
+ /** The `thead` and `tbody`, drawn. */
139
+ readonly children: ReactNode;
140
+ }
141
+ /** A fenced block of code other than a diagram. */
142
+ export interface CodeBlockSlotProps {
143
+ /** The info string's language, or `null`. */
144
+ readonly language: string | null;
145
+ /** The code exactly as typed, which is what Copy copies. */
146
+ readonly code: string;
147
+ /** The code drawn: the `<code>` element, highlighted when the language is known. */
148
+ readonly children: ReactNode;
149
+ /** The Copy button, drawn; a theme places it or leaves it out. */
150
+ readonly copy: ReactNode;
151
+ }
152
+ /** A code span. */
153
+ export interface InlineCodeSlotProps {
154
+ readonly children: ReactNode;
155
+ }
156
+ /** A heading. */
157
+ export interface HeadingSlotProps {
158
+ readonly level: 1 | 2 | 3 | 4 | 5 | 6;
159
+ readonly children: ReactNode;
160
+ }
161
+ /** A quotation that is not a callout. */
162
+ export interface BlockquoteSlotProps {
163
+ readonly children: ReactNode;
164
+ }
165
+ /** A note embedded in place (§7.3), in each state the page shows. */
166
+ export interface EmbedSlotProps {
167
+ /**
168
+ * `ready`: the content and its caption; `section-missing`: the note answered
169
+ * and the section or block did not; `loading`: the host has not answered
170
+ * yet; `pending`: no single note answers the target.
171
+ */
172
+ readonly state: 'ready' | 'section-missing' | 'loading' | 'pending';
173
+ /** The target as written, and its anchor. */
174
+ readonly target: string;
175
+ readonly anchor: string | null;
176
+ /** The note it came from, linked through the host when it has an address; `null` until it is known. */
177
+ readonly caption: ReactNode;
178
+ /** What the default theme draws for this state: the content, the notice, the skeleton or the pending marker. */
179
+ readonly children: ReactNode;
180
+ }
181
+ /** The notes of a note, gathered at its end (§7.12). */
182
+ export interface FootnotesSlotProps {
183
+ /** The heading, in the reader's words. */
184
+ readonly label: string;
185
+ /** The ordered list of the notes, each with its way back. */
186
+ readonly children: ReactNode;
187
+ }
188
+ /** A task box (§7.4). */
189
+ export interface TaskBoxSlotProps {
190
+ readonly checked: boolean;
191
+ /** Whether a tick is reported: the note is writable and the box can be mapped back to its bytes. */
192
+ readonly enabled: boolean;
193
+ /** Reports the tick to the host; does nothing when the box is not enabled. */
194
+ readonly toggle: () => void;
195
+ }
196
+ /** A wikilink to a note (§5.2). */
197
+ export interface WikilinkSlotProps {
198
+ /** True when no note answers the target: the link is drawn as pending. */
199
+ readonly pending: boolean;
200
+ /** The text of the link. */
201
+ readonly children: ReactNode;
202
+ /**
203
+ * Draws a label as the link — through the host's router to the address the
204
+ * host resolved, or as the pending marker. It is the only way to the
205
+ * address: a slot decorates the link and never chooses where it goes.
206
+ */
207
+ readonly link: (label: ReactNode) => ReactNode;
208
+ }
209
+ /** A `mermaid` fence (§7.2). */
210
+ export interface DiagramSlotProps {
211
+ readonly source: string;
212
+ /** `loading` while Mermaid draws; `failed` when it cannot, and the source is shown. */
213
+ readonly state: 'loading' | 'ready' | 'failed';
214
+ /** What the default theme draws for this state: the SVG, the space it will take, or the source. */
215
+ readonly children: ReactNode;
216
+ }
217
+ /**
218
+ * A note's frontmatter (§6), read: what the theme draws above the body, or does not. The default
219
+ * theme draws `name` as the title and each entry by its shape.
220
+ */
221
+ export interface PropertiesSlotProps {
222
+ /** `name:` when it is a single text value, else `null` (§6.5). */
223
+ readonly name: string | null;
224
+ /** Every other entry, in the order written, each with its shape (§6.3). */
225
+ readonly entries: readonly PropertyEntry[];
226
+ /**
227
+ * Draws a value as the note means it: each wikilink in it as a link the host resolved — the
228
+ * note, the choice or the pending marker — and the rest as text. It is the only way to those
229
+ * links: a slot decides how a value looks, never where its links go.
230
+ */
231
+ readonly value: (text: string) => ReactNode;
232
+ }
233
+ /** The slots a theme may fill. Each one left out is drawn as the default theme draws it. */
234
+ export interface ThemeComponents {
235
+ readonly callout: (props: CalloutSlotProps) => ReactNode;
236
+ readonly calloutIcon: (props: {
237
+ readonly kind: string;
238
+ }) => ReactNode;
239
+ readonly table: (props: TableSlotProps) => ReactNode;
240
+ readonly codeBlock: (props: CodeBlockSlotProps) => ReactNode;
241
+ readonly inlineCode: (props: InlineCodeSlotProps) => ReactNode;
242
+ readonly heading: (props: HeadingSlotProps) => ReactNode;
243
+ readonly blockquote: (props: BlockquoteSlotProps) => ReactNode;
244
+ readonly embed: (props: EmbedSlotProps) => ReactNode;
245
+ readonly footnotes: (props: FootnotesSlotProps) => ReactNode;
246
+ readonly taskBox: (props: TaskBoxSlotProps) => ReactNode;
247
+ readonly wikilink: (props: WikilinkSlotProps) => ReactNode;
248
+ readonly diagram: (props: DiagramSlotProps) => ReactNode;
249
+ readonly properties: (props: PropertiesSlotProps) => ReactNode;
250
+ }
251
+ /** The names of the slots, in the order the documentation lists them. */
252
+ export declare const THEME_SLOTS: (keyof ThemeComponents)[];
253
+ export interface Theme {
254
+ /**
255
+ * What the root of a note carries as `markdown-theme-<name>`, which scopes the
256
+ * theme's own CSS: lower case, a letter first, then letters, digits and
257
+ * hyphens.
258
+ */
259
+ readonly name: string;
260
+ /** The schemes the theme draws in. Defaults to both; a theme of one is drawn in it whatever is asked. */
261
+ readonly schemes?: readonly Scheme[];
262
+ /** Values for every scheme, or for each scheme apart. */
263
+ readonly properties?: ThemeProperties | {
264
+ readonly light?: ThemeProperties;
265
+ readonly dark?: ThemeProperties;
266
+ };
267
+ /** An icon per callout kind, drawn in the title in place of the stylesheet's. */
268
+ readonly icons?: Readonly<Record<string, ReactNode>>;
269
+ /** The slots the theme fills. */
270
+ readonly components?: Partial<ThemeComponents>;
271
+ }
272
+ /**
273
+ * A theme, checked: its name can be a class, and every property and slot it
274
+ * names exists. It returns the theme as given; define it once, outside a
275
+ * component, so the note is not drawn again on every render.
276
+ */
277
+ export declare function defineTheme<T extends Theme>(theme: T): T;
278
+ /** MemorySmith's look: the package's stylesheet, with nothing set over it. */
279
+ export declare const DEFAULT_THEME: Theme;
280
+ /** The scheme a theme is drawn in, given the one asked for. */
281
+ export declare function schemeOf(theme: Theme, asked: Scheme): Scheme;
282
+ /** The custom properties the root of a note carries for a theme in a scheme. */
283
+ export declare function rootStyle(theme: Theme, scheme: Scheme): Record<string, string>;
package/dist/theme.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Themes: how a note looks, written by whoever draws it.
3
+ *
4
+ * A theme decides how a note LOOKS, at three levels a developer can mix:
5
+ *
6
+ * 1. **Properties** — the values the stylesheet reads, by name. The renderer
7
+ * writes them on the root of the note as `--markdown-*` custom properties,
8
+ * so a theme that only sets values needs no CSS of its own.
9
+ * 2. **Its own CSS** — the root carries `markdown-theme-<name>`, so a theme's
10
+ * stylesheet reaches anything inside the note and nothing outside it.
11
+ * 3. **Components** — a closed set of slots (`ThemeComponents`), each given the
12
+ * notation already interpreted, which only draw it.
13
+ *
14
+ * What a notation MEANS is never a theme's: the parsing, the resolution of a
15
+ * wikilink, the address filter, the refusal of raw HTML, the ordinal a task box
16
+ * reports and the numbering of footnotes happen before a slot is called, and a
17
+ * slot is handed their result and no way around it.
18
+ *
19
+ * MemorySmith's look is the theme `default`: the package's stylesheet with no
20
+ * property set. It is the theme every other one starts from, since a property
21
+ * is read by the default theme's rules.
22
+ */
23
+ /** The names of `ThemeProperties`, in the order the documentation lists them. */
24
+ export const THEME_PROPERTIES = Object.keys({
25
+ bg: 1, surface: 1, border: 1, text: 1, textSoft: 1, accent: 1, pending: 1,
26
+ codeBg: 1, codeColor: 1, codeBorder: 1, inlineCodeBg: 1, inlineCodePadding: 1,
27
+ tokenKeyword: 1, tokenString: 1, tokenNumber: 1, tokenComment: 1, tokenPunctuation: 1,
28
+ calloutInfo: 1, calloutIdea: 1, calloutDone: 1, calloutWarn: 1, calloutStop: 1, calloutAside: 1, calloutQuote: 1,
29
+ iconNote: 1, iconInfo: 1, iconSummary: 1, iconTip: 1, iconSuccess: 1, iconQuestion: 1, iconWarning: 1,
30
+ iconFailure: 1, iconDanger: 1, iconExample: 1, iconQuote: 1,
31
+ fontSans: 1, fontDisplay: 1, fontMono: 1, fontHeading: 1,
32
+ radius: 1, radiusLarge: 1,
33
+ fontSize: 1, lineHeight: 1, lineHeightTight: 1, rootLineHeight: 1, paragraphSpacing: 1, headingSpacing: 1,
34
+ listIndent: 1, listSpacing: 1,
35
+ headingWeight: 1, headingColor: 1, h1Size: 1, h2Size: 1, h3Size: 1, h4Size: 1, h5Size: 1, h6Size: 1,
36
+ h1Weight: 1, h6Color: 1,
37
+ quoteBorderColor: 1, quoteBorderWidth: 1, quoteColor: 1,
38
+ tableBorder: 1, tableCellPadding: 1, tableHeaderBg: 1, tableHeaderRule: 1, tableStripeBg: 1, tableHoverBg: 1,
39
+ embedRule: 1,
40
+ titleFont: 1, titleSize: 1, titleWeight: 1, titleColor: 1,
41
+ propertiesRule: 1, propertiesLabelFont: 1, propertiesLabelColor: 1, propertiesKeyColor: 1,
42
+ iconPropertyText: 1, iconPropertyList: 1, iconPropertyDate: 1, iconPropertyBoolean: 1,
43
+ chipBg: 1, chipColor: 1, chipFont: 1,
44
+ });
45
+ /** `textSoft` → `--markdown-text-soft`, `h1Size` → `--markdown-h1-size`. */
46
+ export function customPropertyOf(name) {
47
+ return `--markdown-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
48
+ }
49
+ /** The names of the slots, in the order the documentation lists them. */
50
+ export const THEME_SLOTS = Object.keys({
51
+ callout: 1, calloutIcon: 1, table: 1, codeBlock: 1, inlineCode: 1, heading: 1, blockquote: 1,
52
+ embed: 1, footnotes: 1, taskBox: 1, wikilink: 1, diagram: 1, properties: 1,
53
+ });
54
+ const NAME = /^[a-z][a-z0-9-]*$/;
55
+ /**
56
+ * A theme, checked: its name can be a class, and every property and slot it
57
+ * names exists. It returns the theme as given; define it once, outside a
58
+ * component, so the note is not drawn again on every render.
59
+ */
60
+ export function defineTheme(theme) {
61
+ if (!NAME.test(theme.name)) {
62
+ throw new Error(`A theme's name is lower case, a letter first, then letters, digits and hyphens: "${theme.name}"`);
63
+ }
64
+ const known = new Set(THEME_PROPERTIES);
65
+ for (const values of propertySets(theme)) {
66
+ for (const key of Object.keys(values)) {
67
+ if (!known.has(key))
68
+ throw new Error(`The theme "${theme.name}" sets "${key}", which is not a theme property`);
69
+ }
70
+ }
71
+ const slots = new Set(THEME_SLOTS);
72
+ for (const key of Object.keys(theme.components ?? {})) {
73
+ if (!slots.has(key))
74
+ throw new Error(`The theme "${theme.name}" fills "${key}", which is not a slot`);
75
+ }
76
+ return theme;
77
+ }
78
+ /** MemorySmith's look: the package's stylesheet, with nothing set over it. */
79
+ export const DEFAULT_THEME = { name: 'default' };
80
+ function isPerScheme(properties) {
81
+ return 'light' in properties || 'dark' in properties;
82
+ }
83
+ function propertySets(theme) {
84
+ const properties = theme.properties;
85
+ if (!properties)
86
+ return [];
87
+ return isPerScheme(properties) ? [properties.light ?? {}, properties.dark ?? {}] : [properties];
88
+ }
89
+ /** The scheme a theme is drawn in, given the one asked for. */
90
+ export function schemeOf(theme, asked) {
91
+ const schemes = theme.schemes ?? ['light', 'dark'];
92
+ return schemes.includes(asked) ? asked : (schemes[0] ?? asked);
93
+ }
94
+ /** The custom properties the root of a note carries for a theme in a scheme. */
95
+ export function rootStyle(theme, scheme) {
96
+ const properties = theme.properties;
97
+ if (!properties)
98
+ return {};
99
+ const values = isPerScheme(properties) ? (properties[scheme] ?? {}) : properties;
100
+ const style = {};
101
+ for (const name of THEME_PROPERTIES) {
102
+ const value = values[name];
103
+ if (value !== undefined)
104
+ style[customPropertyOf(name)] = value;
105
+ }
106
+ return style;
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorysmithapp/markdown-renderer",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "A React component that draws a MemorySmith note exactly as MemorySmith's reading page does.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -31,6 +31,8 @@
31
31
  "dist/*.js",
32
32
  "dist/*.d.ts",
33
33
  "styles.css",
34
+ "structure.css",
35
+ "theme-default.css",
34
36
  "LICENSE",
35
37
  "README.md"
36
38
  ],
@@ -40,7 +42,13 @@
40
42
  "types": "./dist/index.d.ts",
41
43
  "default": "./dist/index.js"
42
44
  },
43
- "./styles.css": "./styles.css"
45
+ "./diagnose": {
46
+ "types": "./dist/diagnose.d.ts",
47
+ "default": "./dist/diagnose.js"
48
+ },
49
+ "./styles.css": "./styles.css",
50
+ "./structure.css": "./structure.css",
51
+ "./theme-default.css": "./theme-default.css"
44
52
  },
45
53
  "peerDependencies": {
46
54
  "katex": "^0.18.5",
@@ -59,7 +67,7 @@
59
67
  "unified": "11.0.5"
60
68
  },
61
69
  "devDependencies": {
62
- "@memorysmithapp/markdown-spec": "0.1.2",
70
+ "@memorysmithapp/markdown-spec": "0.3.0",
63
71
  "@types/hast": "3.0.5",
64
72
  "@types/react": "19.2.18",
65
73
  "@types/react-dom": "19.2.4",
package/structure.css ADDED
@@ -0,0 +1,57 @@
1
+ /*
2
+ * What every theme needs, whatever it looks like: the reading surface working
3
+ * before it is drawn in anybody's colours. A theme that draws everything
4
+ * itself imports this file alone; `styles.css` is this file and the default
5
+ * theme, `theme-default.css`, together.
6
+ *
7
+ * Every selector is under `.markdown`, so the package styles the note and
8
+ * never the page. Nothing here reads a colour, a face or a metric.
9
+ */
10
+
11
+ .markdown,
12
+ .markdown *,
13
+ .markdown *::before,
14
+ .markdown *::after {
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ .markdown img {
19
+ max-width: 100%;
20
+ }
21
+
22
+ .markdown .mermaid-diagram svg {
23
+ max-width: 100%;
24
+ height: auto;
25
+ }
26
+
27
+ /* The visible line went away; the word has to stay for whoever hears it. */
28
+ .markdown .visually-hidden {
29
+ position: absolute;
30
+ width: 1px;
31
+ height: 1px;
32
+ margin: -1px;
33
+ padding: 0;
34
+ overflow: hidden;
35
+ clip-path: inset(50%);
36
+ white-space: nowrap;
37
+ border: 0;
38
+ }
39
+
40
+ /* An icon a theme draws as an element takes the place of the one the
41
+ stylesheet draws as a mask, at the size of the title it sits in. */
42
+ .markdown .callout-title.has-icon::before {
43
+ display: none;
44
+ }
45
+ .markdown .callout-icon {
46
+ display: inline-flex;
47
+ flex: 0 0 auto;
48
+ align-items: center;
49
+ justify-content: center;
50
+ width: 1.1em;
51
+ height: 1.1em;
52
+ margin-top: 0.1em;
53
+ }
54
+ .markdown .callout-icon > svg {
55
+ width: 100%;
56
+ height: 100%;
57
+ }