@jarenjs/md 0.34.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.
- package/README.md +520 -0
- package/dist/types/ast.d.ts +181 -0
- package/dist/types/bake.d.ts +61 -0
- package/dist/types/compiler.d.ts +141 -0
- package/dist/types/component/index.d.ts +101 -0
- package/dist/types/directives.d.ts +126 -0
- package/dist/types/entities.d.ts +40 -0
- package/dist/types/footnotes.d.ts +83 -0
- package/dist/types/frontmatter.d.ts +67 -0
- package/dist/types/html.d.ts +72 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/loader.d.ts +84 -0
- package/dist/types/mdx.d.ts +45 -0
- package/dist/types/parser.d.ts +116 -0
- package/dist/types/plugins/highlight.d.ts +64 -0
- package/dist/types/plugins/index.d.ts +64 -0
- package/dist/types/plugins/mermaid.d.ts +12 -0
- package/dist/types/scanner.d.ts +240 -0
- package/dist/types/to-html.d.ts +104 -0
- package/dist/types/to-md.d.ts +23 -0
- package/dist/types/to-vnode.d.ts +161 -0
- package/dist/types/utils.d.ts +63 -0
- package/docs/LOADER.md +92 -0
- package/docs/MD-FORMAT.md +502 -0
- package/docs/PLUGINS.md +277 -0
- package/package.json +80 -0
- package/schemas/jaren-md-ast.schema.json +296 -0
- package/src/ast.js +346 -0
- package/src/bake.js +104 -0
- package/src/compiler.js +167 -0
- package/src/component/index.js +191 -0
- package/src/directives.js +371 -0
- package/src/entities.js +107 -0
- package/src/footnotes.js +180 -0
- package/src/frontmatter.js +947 -0
- package/src/html.js +281 -0
- package/src/index.js +76 -0
- package/src/loader.js +0 -0
- package/src/mdx.js +219 -0
- package/src/parser.js +1685 -0
- package/src/plugins/highlight.js +325 -0
- package/src/plugins/index.js +75 -0
- package/src/plugins/mermaid.js +14 -0
- package/src/scanner.js +832 -0
- package/src/to-html.js +425 -0
- package/src/to-md.js +396 -0
- package/src/to-vnode.js +766 -0
- package/src/utils.js +107 -0
- package/styles/md.css +238 -0
package/src/utils.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Small shared helpers for the md package.
|
|
4
|
+
*
|
|
5
|
+
* The content hash is the package's identity primitive — block vnode
|
|
6
|
+
* keys, the document `meta.hash`, and the mermaid SVG cache are all keyed
|
|
7
|
+
* by it — so md re-exports the suite's single `hashContent` from
|
|
8
|
+
* `@jarenjs/core` rather than carrying its own copy; equal content hits
|
|
9
|
+
* O(1) fast paths everywhere downstream. `fnv1a` is that same mixing
|
|
10
|
+
* step, exposed for the two callers that fold a hash incrementally (the
|
|
11
|
+
* streaming parser's chunks, the structural block-key walk) and so must
|
|
12
|
+
* seed it themselves from `FNV1A_OFFSET_BASIS`. Heading slugs come from
|
|
13
|
+
* the same place for the same reason: `slugify` is a pure text→fragment
|
|
14
|
+
* transform with no Markdown knowledge, so the suite keeps exactly one
|
|
15
|
+
* of it. The remaining helpers are md's own allocation-light scanner
|
|
16
|
+
* utilities.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { slugify } from '@jarenjs/core/string';
|
|
20
|
+
|
|
21
|
+
export { hashContent, fnv1a, FNV1A_OFFSET_BASIS, slugify } from '@jarenjs/core/string';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The `id` for one heading, unique within one emission.
|
|
25
|
+
*
|
|
26
|
+
* Both emitters mint ids, so the rule lives here once: slug the text,
|
|
27
|
+
* substitute `section` when nothing slug-worthy survives, number
|
|
28
|
+
* repeats the way GitHub numbers them (`setup`, `setup-1`, `setup-2`)
|
|
29
|
+
* and prefix the result. The COUNTER belongs to the caller — one map per
|
|
30
|
+
* emission, never shared with another numbering (a block key's hash and
|
|
31
|
+
* a slug share a namespace only by accident, and a collision there would
|
|
32
|
+
* shift an unrelated heading's number).
|
|
33
|
+
*
|
|
34
|
+
* @param {string} text the heading's plain text (`textOf`)
|
|
35
|
+
* @param {Map<string, number>} seen the emission's slug counter
|
|
36
|
+
* @param {string} prefix prepended to the result
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function headingId(text, seen, prefix) {
|
|
40
|
+
const base = slugify(text) || 'section';
|
|
41
|
+
const count = seen.get(base) ?? 0;
|
|
42
|
+
seen.set(base, count + 1);
|
|
43
|
+
return prefix + (count === 0 ? base : base + '-' + count);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The accessible name for a heading's permalink affordance — `#` alone
|
|
48
|
+
* names nothing, so the link says which section it points at.
|
|
49
|
+
* @param {string} text the heading's plain text (`textOf`)
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function permalinkLabel(text) {
|
|
53
|
+
const trimmed = text.trim();
|
|
54
|
+
return trimmed === '' ? 'Permalink to this section' : 'Permalink to ' + trimmed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Count leading space characters (U+0020 only; the scanner expands no
|
|
59
|
+
* tabs here — callers pass detabbed text).
|
|
60
|
+
* @param {string} line
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
export function countIndent(line) {
|
|
64
|
+
let i = 0;
|
|
65
|
+
while (i < line.length && line.charCodeAt(i) === 32) i++;
|
|
66
|
+
return i;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Is the line blank (empty or whitespace-only)?
|
|
71
|
+
* @param {string} line
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
export function isBlankLine(line) {
|
|
75
|
+
for (let i = 0; i < line.length; i++) {
|
|
76
|
+
const c = line.charCodeAt(i);
|
|
77
|
+
if (c !== 32 && c !== 9) return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Replace tabs with spaces to the next 4-column tab stop, counting
|
|
84
|
+
* columns from `startColumn`. Lines without tabs return the same
|
|
85
|
+
* string reference (the common case allocates nothing).
|
|
86
|
+
* @param {string} line
|
|
87
|
+
* @param {number} [startColumn]
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
export function expandTabs(line, startColumn = 0) {
|
|
91
|
+
if (line.indexOf('\t') === -1) return line;
|
|
92
|
+
let out = '';
|
|
93
|
+
let column = startColumn;
|
|
94
|
+
for (let i = 0; i < line.length; i++) {
|
|
95
|
+
const ch = line[i];
|
|
96
|
+
if (ch === '\t') {
|
|
97
|
+
const width = 4 - (column % 4);
|
|
98
|
+
out += ' '.slice(0, width);
|
|
99
|
+
column += width;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
out += ch;
|
|
103
|
+
column++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
package/styles/md.css
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @jarenjs/md — the visual component's stylesheet.
|
|
3
|
+
*
|
|
4
|
+
* Scoped under the `.md` article the vnode emitter produces, plus the
|
|
5
|
+
* `tok-*` token classes of the highlight plugin (docs/PLUGINS.md §6.2)
|
|
6
|
+
* and the mermaid placeholder. Colors come in a light set and a dark
|
|
7
|
+
* override activated by a `.dark` class on any ancestor (the
|
|
8
|
+
* convention the jarenjs website uses); everything sizes in em so the
|
|
9
|
+
* component inherits its host's type scale.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
.md {
|
|
13
|
+
line-height: 1.6;
|
|
14
|
+
overflow-wrap: break-word;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.md > :first-child { margin-top: 0; }
|
|
18
|
+
.md > :last-child { margin-bottom: 0; }
|
|
19
|
+
|
|
20
|
+
.md h1, .md h2, .md h3, .md h4, .md h5, .md h6 {
|
|
21
|
+
line-height: 1.25;
|
|
22
|
+
margin: 1.4em 0 0.5em;
|
|
23
|
+
}
|
|
24
|
+
.md h1 { font-size: 1.7em; }
|
|
25
|
+
.md h2 { font-size: 1.4em; }
|
|
26
|
+
.md h3 { font-size: 1.2em; }
|
|
27
|
+
|
|
28
|
+
/* A fragment scrolled into view lands under a sticky header unless the
|
|
29
|
+
target reserves the room itself. That is a layout concern, so it lives
|
|
30
|
+
here and not in whatever scrolls the page: a host with a header of its
|
|
31
|
+
own sets `--md-scroll-margin`, one without keeps a little breathing
|
|
32
|
+
room above the heading. */
|
|
33
|
+
.md :is(h1, h2, h3, h4, h5, h6) { scroll-margin-top: var(--md-scroll-margin, 4.5rem); }
|
|
34
|
+
|
|
35
|
+
/* --- heading anchors (`headingAnchors: true`) ---------------------- */
|
|
36
|
+
/* The affordance is quiet until the heading is hovered or the link is
|
|
37
|
+
focused, so a document does not read as a column of `#`. It stays in
|
|
38
|
+
the DOM either way — hiding it with `display: none` would take it off
|
|
39
|
+
the keyboard, and it is the keyboard path that needs it most. */
|
|
40
|
+
.md .md-anchor {
|
|
41
|
+
margin-left: 0.35em;
|
|
42
|
+
color: var(--md-muted, #5b6472);
|
|
43
|
+
font-weight: 400;
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
opacity: 0;
|
|
46
|
+
transition: opacity 120ms ease;
|
|
47
|
+
}
|
|
48
|
+
.md :is(h1, h2, h3, h4, h5, h6):hover .md-anchor,
|
|
49
|
+
.md .md-anchor:focus-visible { opacity: 1; }
|
|
50
|
+
.md .md-anchor:hover { color: var(--md-link, #2456c4); }
|
|
51
|
+
.md .md-anchor:focus-visible {
|
|
52
|
+
outline: 2px solid var(--md-link, #2456c4);
|
|
53
|
+
outline-offset: 2px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@media (prefers-reduced-motion: reduce) {
|
|
57
|
+
.md .md-anchor { transition: none; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.md p, .md ul, .md ol, .md table { margin: 0.6em 0; }
|
|
61
|
+
.md ul, .md ol { padding-left: 1.5em; }
|
|
62
|
+
.md li > ul, .md li > ol { margin: 0.15em 0; }
|
|
63
|
+
.md li { margin: 0.15em 0; }
|
|
64
|
+
|
|
65
|
+
.md blockquote {
|
|
66
|
+
margin: 0.8em 0;
|
|
67
|
+
padding: 0.1em 1em;
|
|
68
|
+
border-left: 3px solid var(--md-rule, #d0d4dc);
|
|
69
|
+
color: var(--md-muted, #5b6472);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.md hr {
|
|
73
|
+
border: 0;
|
|
74
|
+
border-top: 1px solid var(--md-rule, #d0d4dc);
|
|
75
|
+
margin: 1.4em 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.md a { color: var(--md-link, #2456c4); }
|
|
79
|
+
|
|
80
|
+
.md code {
|
|
81
|
+
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, monospace;
|
|
82
|
+
font-size: 0.92em;
|
|
83
|
+
background: var(--md-code-bg, #f2f4f8);
|
|
84
|
+
border-radius: 4px;
|
|
85
|
+
padding: 0.1em 0.35em;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.md pre {
|
|
89
|
+
margin: 0.8em 0;
|
|
90
|
+
padding: 0.8em 1em;
|
|
91
|
+
overflow-x: auto;
|
|
92
|
+
background: var(--md-code-bg, #f2f4f8);
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
}
|
|
95
|
+
.md pre code {
|
|
96
|
+
background: none;
|
|
97
|
+
padding: 0;
|
|
98
|
+
font-size: 0.88em;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.md table {
|
|
102
|
+
border-collapse: collapse;
|
|
103
|
+
display: block;
|
|
104
|
+
overflow-x: auto;
|
|
105
|
+
}
|
|
106
|
+
.md th, .md td {
|
|
107
|
+
border: 1px solid var(--md-rule, #d0d4dc);
|
|
108
|
+
padding: 0.35em 0.7em;
|
|
109
|
+
}
|
|
110
|
+
.md th { background: var(--md-code-bg, #f2f4f8); }
|
|
111
|
+
|
|
112
|
+
.md img { max-width: 100%; }
|
|
113
|
+
|
|
114
|
+
/* --- GFM footnotes ------------------------------------------------- */
|
|
115
|
+
/* The section is appended after the last block of the document, so it
|
|
116
|
+
needs the rule a reader expects between a document and its notes —
|
|
117
|
+
the emitter writes no `<hr>` of its own, because that would be a
|
|
118
|
+
thematic break the author did not write. Small type, muted, and the
|
|
119
|
+
back-arrow sized so a thumb can hit it. */
|
|
120
|
+
.md .footnotes {
|
|
121
|
+
margin-top: 2em;
|
|
122
|
+
padding-top: 0.8em;
|
|
123
|
+
border-top: 1px solid var(--md-rule, #d0d4dc);
|
|
124
|
+
font-size: 0.9em;
|
|
125
|
+
color: var(--md-muted, #5b6472);
|
|
126
|
+
}
|
|
127
|
+
.md .footnotes > ol { margin: 0; }
|
|
128
|
+
.md .footnotes li { margin: 0.3em 0; }
|
|
129
|
+
.md .footnotes p { margin: 0.3em 0; }
|
|
130
|
+
/* The cited note is highlighted when a reader jumps to it, so a landing
|
|
131
|
+
in the middle of a list of notes says which one it landed on. */
|
|
132
|
+
.md .footnotes li:target { background: var(--md-code-bg, #f2f4f8); }
|
|
133
|
+
|
|
134
|
+
.md .footnote-backref {
|
|
135
|
+
text-decoration: none;
|
|
136
|
+
padding: 0 0.25em;
|
|
137
|
+
font-size: 1.1em;
|
|
138
|
+
line-height: 1;
|
|
139
|
+
}
|
|
140
|
+
.md .footnote-backref sup { font-size: 0.7em; }
|
|
141
|
+
|
|
142
|
+
/* A citation is small and easy to miss; give it a target a pointer can
|
|
143
|
+
land on without shifting the line it sits in. */
|
|
144
|
+
.md sup > a { text-decoration: none; padding: 0 0.15em; }
|
|
145
|
+
.md sup > a:hover { text-decoration: underline; }
|
|
146
|
+
|
|
147
|
+
.md input[type='checkbox'] { margin-right: 0.4em; }
|
|
148
|
+
|
|
149
|
+
/* --- highlight plugin token colors (PLUGINS.md §6.2) --------------- */
|
|
150
|
+
|
|
151
|
+
.md .tok-kw { color: var(--md-tok-kw, #0550ae); }
|
|
152
|
+
.md .tok-str { color: var(--md-tok-str, #50a14f); }
|
|
153
|
+
.md .tok-num { color: var(--md-tok-num, #b76b01); }
|
|
154
|
+
.md .tok-com { color: var(--md-tok-com, #8b919c); font-style: italic; }
|
|
155
|
+
.md .tok-pun { color: var(--md-tok-pun, #526069); }
|
|
156
|
+
.md .tok-op { color: var(--md-tok-op, #0184bc); }
|
|
157
|
+
.md .tok-lit { color: var(--md-tok-lit, #b76b01); }
|
|
158
|
+
|
|
159
|
+
/* --- mermaid placeholder / upgrade (PLUGINS.md §6.1) --------------- */
|
|
160
|
+
|
|
161
|
+
.md-mermaid {
|
|
162
|
+
/* A diagram is a figure, not a run of text: giving it a surface and a
|
|
163
|
+
frame separates it from the prose around it and stops a wide diagram
|
|
164
|
+
from reading as part of the paragraph above. */
|
|
165
|
+
margin: 1.2em 0;
|
|
166
|
+
padding: 1rem;
|
|
167
|
+
text-align: center;
|
|
168
|
+
background: var(--md-figure-bg, #fbfcfe);
|
|
169
|
+
border: 1px solid var(--md-rule, #d0d4dc);
|
|
170
|
+
border-radius: 10px;
|
|
171
|
+
}
|
|
172
|
+
.md-mermaid .md-mermaid-src {
|
|
173
|
+
text-align: left;
|
|
174
|
+
color: var(--md-muted, #5b6472);
|
|
175
|
+
}
|
|
176
|
+
/* The diagram's own sizing (natural size, scroll rather than shrink) lives in
|
|
177
|
+
@jarenjs/mermaid's stylesheet, which owns the element the plugin emits. */
|
|
178
|
+
|
|
179
|
+
/* --- the optional interactive viewer ------------------------------- */
|
|
180
|
+
/* Only present once `mermaidPlugin({ interactive: true })` has hydrated:
|
|
181
|
+
the server-rendered figure carries none of this. */
|
|
182
|
+
.md-mermaid.mm-interactive {
|
|
183
|
+
position: relative;
|
|
184
|
+
cursor: grab;
|
|
185
|
+
}
|
|
186
|
+
.md-mermaid.mm-interactive:focus-visible {
|
|
187
|
+
outline: 2px solid var(--md-link, #2563eb);
|
|
188
|
+
outline-offset: 2px;
|
|
189
|
+
}
|
|
190
|
+
.md-mermaid.mm-interactive:active { cursor: grabbing; }
|
|
191
|
+
.md-mermaid .mm-controls {
|
|
192
|
+
position: absolute;
|
|
193
|
+
right: 0.5rem;
|
|
194
|
+
bottom: 0.5rem;
|
|
195
|
+
display: flex;
|
|
196
|
+
gap: 0.25rem;
|
|
197
|
+
opacity: 0.35;
|
|
198
|
+
transition: opacity 120ms ease;
|
|
199
|
+
}
|
|
200
|
+
.md-mermaid:hover .mm-controls,
|
|
201
|
+
.md-mermaid:focus-within .mm-controls { opacity: 1; }
|
|
202
|
+
.md-mermaid .mm-control {
|
|
203
|
+
/* 32px is below the 44px touch target the site requires elsewhere, so
|
|
204
|
+
these are a convenience: every action they offer is also on the
|
|
205
|
+
keyboard and in the pinch gesture. */
|
|
206
|
+
min-width: 2rem;
|
|
207
|
+
min-height: 2rem;
|
|
208
|
+
padding: 0;
|
|
209
|
+
font-size: 1rem;
|
|
210
|
+
line-height: 1;
|
|
211
|
+
color: var(--md-muted, #5b6472);
|
|
212
|
+
background: var(--md-figure-bg, #fbfcfe);
|
|
213
|
+
border: 1px solid var(--md-rule, #d0d4dc);
|
|
214
|
+
border-radius: 8px;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
}
|
|
217
|
+
.md-mermaid .mm-control:hover { color: var(--md-link, #2563eb); }
|
|
218
|
+
|
|
219
|
+
@media (prefers-reduced-motion: reduce) {
|
|
220
|
+
.md-mermaid .mm-controls { transition: none; }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* --- dark scheme --------------------------------------------------- */
|
|
224
|
+
|
|
225
|
+
.dark .md, .md.dark {
|
|
226
|
+
--md-figure-bg: #171b22;
|
|
227
|
+
--md-rule: #39414e;
|
|
228
|
+
--md-muted: #9aa4b2;
|
|
229
|
+
--md-link: #7aa2ff;
|
|
230
|
+
--md-code-bg: #1d232c;
|
|
231
|
+
--md-tok-kw: #79b8ff;
|
|
232
|
+
--md-tok-str: #98c379;
|
|
233
|
+
--md-tok-num: #d19a66;
|
|
234
|
+
--md-tok-com: #7f848e;
|
|
235
|
+
--md-tok-pun: #9aa4b2;
|
|
236
|
+
--md-tok-op: #56b6c2;
|
|
237
|
+
--md-tok-lit: #d19a66;
|
|
238
|
+
}
|