@markupme/core 0.0.0-stage → 0.1.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/dist/format.d.ts +3 -0
- package/dist/format.js +5 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/parse.d.ts +6 -0
- package/dist/parse.js +66 -0
- package/dist/plugins/alerts.d.ts +8 -0
- package/dist/plugins/alerts.js +50 -0
- package/dist/plugins/directives.d.ts +16 -0
- package/dist/plugins/directives.js +126 -0
- package/dist/plugins/emoji.d.ts +3 -0
- package/dist/plugins/emoji.js +22 -0
- package/dist/plugins/frontmatter.d.ts +4 -0
- package/dist/plugins/frontmatter.js +37 -0
- package/dist/plugins/headings.d.ts +18 -0
- package/dist/plugins/headings.js +69 -0
- package/dist/plugins/toc.d.ts +18 -0
- package/dist/plugins/toc.js +103 -0
- package/dist/processor.d.ts +46 -0
- package/dist/processor.js +127 -0
- package/dist/render/html.d.ts +10 -0
- package/dist/render/html.js +219 -0
- package/dist/render/schema.d.ts +7 -0
- package/dist/render/schema.js +51 -0
- package/dist/render/text.d.ts +3 -0
- package/dist/render/text.js +48 -0
- package/dist/serialize.d.ts +16 -0
- package/dist/serialize.js +51 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.js +2 -0
- package/dist/util.d.ts +48 -0
- package/dist/util.js +162 -0
- package/dist/validate.d.ts +5 -0
- package/dist/validate.js +209 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +74 -4
- package/README.md +0 -3
package/dist/format.d.ts
ADDED
package/dist/format.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** `file:line:col severity code message (rule)`: the format editors and CI annotate from. */
|
|
2
|
+
export function formatDiagnostic(d) {
|
|
3
|
+
const where = [d.file ?? '<input>', d.line, d.column].filter((x) => x !== undefined).join(':');
|
|
4
|
+
return `${where} ${d.severity} ${d.code} ${d.message} (${d.rule})`;
|
|
5
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { formatDiagnostic } from './format.js';
|
|
2
|
+
export { ALERT_TITLES, ALERT_TYPES, type AlertType } from './plugins/alerts.js';
|
|
3
|
+
export { CALLOUTS, CONTAINERS, LEAF_DIRECTIVES, TEXT_DIRECTIVES } from './plugins/directives.js';
|
|
4
|
+
export { headingId, type OutlineItem, outline } from './plugins/headings.js';
|
|
5
|
+
export { buildToc, insertToc, type TocNode } from './plugins/toc.js';
|
|
6
|
+
export { type BuiltinTransform, createProcessor, MarkupMeError, type Processor, parse, render, serialize, type TransformOptions, type TransformResult, transform, validate, } from './processor.js';
|
|
7
|
+
export { markupmeSchema } from './render/schema.js';
|
|
8
|
+
export type { SerializeFormat, SerializeOptions } from './serialize.js';
|
|
9
|
+
export * from './types.js';
|
|
10
|
+
export { type BlockChange, type DocumentStats, diff, escapeHtml, query, stats, stripPositions, visit, } from './util.js';
|
|
11
|
+
export { RULES } from './validate.js';
|
|
12
|
+
export { VERSION } from './version.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { formatDiagnostic } from './format.js';
|
|
2
|
+
export { ALERT_TITLES, ALERT_TYPES } from './plugins/alerts.js';
|
|
3
|
+
export { CALLOUTS, CONTAINERS, LEAF_DIRECTIVES, TEXT_DIRECTIVES } from './plugins/directives.js';
|
|
4
|
+
export { headingId, outline } from './plugins/headings.js';
|
|
5
|
+
export { buildToc, insertToc } from './plugins/toc.js';
|
|
6
|
+
export { createProcessor, MarkupMeError, parse, render, serialize, transform, validate, } from './processor.js';
|
|
7
|
+
export { markupmeSchema } from './render/schema.js';
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export { diff, escapeHtml, query, stats, stripPositions, visit, } from './util.js';
|
|
10
|
+
export { RULES } from './validate.js';
|
|
11
|
+
export { VERSION } from './version.js';
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type PluggableList } from 'unified';
|
|
2
|
+
import { type MarkupMeDocument, type ParseOptions, type ProcessorOptions, type TocOptions } from './types.js';
|
|
3
|
+
/** Syntax extensions shared by the parser and the serializer, so round trips keep every construct. */
|
|
4
|
+
export declare function syntaxPlugins(opts: ProcessorOptions): PluggableList;
|
|
5
|
+
export declare function tocOptions(opts: ProcessorOptions): TocOptions | undefined;
|
|
6
|
+
export declare function parseDocument(source: string, opts: ProcessorOptions, parseOpts?: ParseOptions): MarkupMeDocument;
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import remarkDirective from 'remark-directive';
|
|
2
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import remarkMath from 'remark-math';
|
|
5
|
+
import remarkParse from 'remark-parse';
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
import { annotateAlerts } from './plugins/alerts.js';
|
|
8
|
+
import { normalizeDirectives } from './plugins/directives.js';
|
|
9
|
+
import { readFrontmatter } from './plugins/frontmatter.js';
|
|
10
|
+
import { addHeadingIds } from './plugins/headings.js';
|
|
11
|
+
import { insertToc } from './plugins/toc.js';
|
|
12
|
+
import { SCHEMA_VERSION, } from './types.js';
|
|
13
|
+
/** Syntax extensions shared by the parser and the serializer, so round trips keep every construct. */
|
|
14
|
+
export function syntaxPlugins(opts) {
|
|
15
|
+
const list = [];
|
|
16
|
+
if (opts.gfm !== false)
|
|
17
|
+
list.push(remarkGfm);
|
|
18
|
+
if (opts.frontmatter !== false)
|
|
19
|
+
list.push([remarkFrontmatter, ['yaml', 'toml']]);
|
|
20
|
+
if (opts.directives !== false)
|
|
21
|
+
list.push(remarkDirective);
|
|
22
|
+
if (opts.math === true)
|
|
23
|
+
list.push(remarkMath);
|
|
24
|
+
for (const p of opts.plugins ?? [])
|
|
25
|
+
if (p.remarkPlugins)
|
|
26
|
+
list.push(...p.remarkPlugins);
|
|
27
|
+
return list;
|
|
28
|
+
}
|
|
29
|
+
export function tocOptions(opts) {
|
|
30
|
+
if (opts.toc === false)
|
|
31
|
+
return undefined;
|
|
32
|
+
return opts.toc === true || opts.toc === undefined ? {} : opts.toc;
|
|
33
|
+
}
|
|
34
|
+
export function parseDocument(source, opts, parseOpts = {}) {
|
|
35
|
+
const processor = unified().use(remarkParse).use(syntaxPlugins(opts));
|
|
36
|
+
const tree = processor.runSync(processor.parse(source));
|
|
37
|
+
const diagnostics = [];
|
|
38
|
+
const file = parseOpts.file;
|
|
39
|
+
const report = (d) => diagnostics.push(file ? { ...d, file } : d);
|
|
40
|
+
const doc = {
|
|
41
|
+
type: 'markupme-document',
|
|
42
|
+
schemaVersion: SCHEMA_VERSION,
|
|
43
|
+
source,
|
|
44
|
+
tree,
|
|
45
|
+
diagnostics,
|
|
46
|
+
...(file ? { file } : {}),
|
|
47
|
+
};
|
|
48
|
+
if (opts.frontmatter !== false) {
|
|
49
|
+
const fm = readFrontmatter(tree, report);
|
|
50
|
+
if (fm)
|
|
51
|
+
doc.frontmatter = fm;
|
|
52
|
+
}
|
|
53
|
+
if (opts.directives !== false)
|
|
54
|
+
normalizeDirectives(tree, report);
|
|
55
|
+
if (opts.alerts !== false)
|
|
56
|
+
annotateAlerts(tree);
|
|
57
|
+
if (opts.headingIds !== false)
|
|
58
|
+
addHeadingIds(tree, report);
|
|
59
|
+
const toc = tocOptions(opts);
|
|
60
|
+
if (toc)
|
|
61
|
+
insertToc(tree, toc);
|
|
62
|
+
const ctx = { doc, options: opts, report };
|
|
63
|
+
for (const p of opts.plugins ?? [])
|
|
64
|
+
p.parse?.(tree, ctx);
|
|
65
|
+
return doc;
|
|
66
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Root } from 'mdast';
|
|
2
|
+
export declare const ALERT_TYPES: readonly ['note', 'tip', 'important', 'warning', 'caution'];
|
|
3
|
+
export type AlertType = (typeof ALERT_TYPES)[number];
|
|
4
|
+
export declare const ALERT_TITLES: Record<AlertType, string>;
|
|
5
|
+
/** Parse time: records `data.alert` on `> [!NOTE]` blockquotes. The tree text is untouched. */
|
|
6
|
+
export declare function annotateAlerts(tree: Root): void;
|
|
7
|
+
/** Render time (on a copy): turns annotated blockquotes into GitHub-style alert boxes. */
|
|
8
|
+
export declare function presentAlerts(tree: Root): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
export const ALERT_TYPES = ['note', 'tip', 'important', 'warning', 'caution'];
|
|
3
|
+
const MARKER = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*(?:\r?\n|$)/i;
|
|
4
|
+
export const ALERT_TITLES = {
|
|
5
|
+
note: 'Note',
|
|
6
|
+
tip: 'Tip',
|
|
7
|
+
important: 'Important',
|
|
8
|
+
warning: 'Warning',
|
|
9
|
+
caution: 'Caution',
|
|
10
|
+
};
|
|
11
|
+
/** Parse time: records `data.alert` on `> [!NOTE]` blockquotes. The tree text is untouched. */
|
|
12
|
+
export function annotateAlerts(tree) {
|
|
13
|
+
visit(tree, 'blockquote', (node) => {
|
|
14
|
+
const first = node.children[0];
|
|
15
|
+
if (first?.type !== 'paragraph')
|
|
16
|
+
return;
|
|
17
|
+
const text = first.children[0];
|
|
18
|
+
if (text?.type !== 'text')
|
|
19
|
+
return;
|
|
20
|
+
const m = MARKER.exec(text.value);
|
|
21
|
+
if (!m?.[1])
|
|
22
|
+
return;
|
|
23
|
+
node.data = { ...node.data, alert: m[1].toLowerCase() };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Render time (on a copy): turns annotated blockquotes into GitHub-style alert boxes. */
|
|
27
|
+
export function presentAlerts(tree) {
|
|
28
|
+
visit(tree, 'blockquote', (node) => {
|
|
29
|
+
const type = node.data?.alert;
|
|
30
|
+
if (!type)
|
|
31
|
+
return;
|
|
32
|
+
const first = node.children[0];
|
|
33
|
+
const text = first.children[0];
|
|
34
|
+
if (text?.type === 'text')
|
|
35
|
+
text.value = text.value.replace(MARKER, '');
|
|
36
|
+
if (first.children.length === 1 && text?.type === 'text' && text.value === '')
|
|
37
|
+
node.children.shift();
|
|
38
|
+
const title = {
|
|
39
|
+
type: 'paragraph',
|
|
40
|
+
children: [{ type: 'text', value: ALERT_TITLES[type] }],
|
|
41
|
+
data: { hProperties: { className: ['markdown-alert-title'] } },
|
|
42
|
+
};
|
|
43
|
+
node.children.unshift(title);
|
|
44
|
+
node.data = {
|
|
45
|
+
...node.data,
|
|
46
|
+
hName: 'div',
|
|
47
|
+
hProperties: { className: ['markdown-alert', `markdown-alert-${type}`], role: 'note' },
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Root } from 'mdast';
|
|
2
|
+
import type { Diagnostic } from '../types.js';
|
|
3
|
+
/** `:::name` containers rendered as callout boxes. */
|
|
4
|
+
export declare const CALLOUTS: string[];
|
|
5
|
+
export declare const CONTAINERS: string[];
|
|
6
|
+
/** `:name[label]` inline directives. */
|
|
7
|
+
export declare const TEXT_DIRECTIVES: string[];
|
|
8
|
+
/** `::name` leaf directives. `toc` is handled by the TOC plugin. */
|
|
9
|
+
export declare const LEAF_DIRECTIVES: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Parse time: a text directive with no label and no attributes (`10:30`, `note:this`) is almost
|
|
12
|
+
* always prose, so it goes back to plain text. Unknown directives are reported, never dropped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeDirectives(tree: Root, report: (d: Diagnostic) => void): void;
|
|
15
|
+
/** Render time (on a copy): maps directives to HTML elements. */
|
|
16
|
+
export declare function presentDirectives(tree: Root): void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { toString as mdToString } from 'mdast-util-to-string';
|
|
2
|
+
import { SKIP, visit } from 'unist-util-visit';
|
|
3
|
+
/** `:::name` containers rendered as callout boxes. */
|
|
4
|
+
export const CALLOUTS = [
|
|
5
|
+
'note',
|
|
6
|
+
'tip',
|
|
7
|
+
'info',
|
|
8
|
+
'important',
|
|
9
|
+
'warning',
|
|
10
|
+
'caution',
|
|
11
|
+
'danger',
|
|
12
|
+
'success',
|
|
13
|
+
];
|
|
14
|
+
export const CONTAINERS = [...CALLOUTS, 'details', 'figure', 'columns', 'column'];
|
|
15
|
+
/** `:name[label]` inline directives. */
|
|
16
|
+
export const TEXT_DIRECTIVES = ['kbd', 'abbr', 'mark', 'sub', 'sup', 'badge'];
|
|
17
|
+
/** `::name` leaf directives. `toc` is handled by the TOC plugin. */
|
|
18
|
+
export const LEAF_DIRECTIVES = ['toc', 'br'];
|
|
19
|
+
function isLabel(node) {
|
|
20
|
+
return (node?.type === 'paragraph' &&
|
|
21
|
+
node.data?.directiveLabel === true);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse time: a text directive with no label and no attributes (`10:30`, `note:this`) is almost
|
|
25
|
+
* always prose, so it goes back to plain text. Unknown directives are reported, never dropped.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeDirectives(tree, report) {
|
|
28
|
+
visit(tree, (node, index, parent) => {
|
|
29
|
+
if (node.type !== 'textDirective' &&
|
|
30
|
+
node.type !== 'leafDirective' &&
|
|
31
|
+
node.type !== 'containerDirective')
|
|
32
|
+
return;
|
|
33
|
+
const d = node;
|
|
34
|
+
const hasAttrs = d.attributes && Object.keys(d.attributes).length > 0;
|
|
35
|
+
if (d.type === 'textDirective' &&
|
|
36
|
+
d.children.length === 0 &&
|
|
37
|
+
!hasAttrs &&
|
|
38
|
+
parent &&
|
|
39
|
+
index !== undefined) {
|
|
40
|
+
parent.children.splice(index, 1, { type: 'text', value: `:${d.name}` });
|
|
41
|
+
return [SKIP, index];
|
|
42
|
+
}
|
|
43
|
+
const known = d.type === 'textDirective'
|
|
44
|
+
? TEXT_DIRECTIVES
|
|
45
|
+
: d.type === 'leafDirective'
|
|
46
|
+
? LEAF_DIRECTIVES
|
|
47
|
+
: CONTAINERS;
|
|
48
|
+
if (!known.includes(d.name)) {
|
|
49
|
+
report({
|
|
50
|
+
code: 'MM009',
|
|
51
|
+
rule: 'unknown-directive',
|
|
52
|
+
severity: 'warning',
|
|
53
|
+
message: `Unknown directive "${d.name}" (${d.type.replace('Directive', '')}); its content is rendered as-is`,
|
|
54
|
+
line: d.position?.start.line,
|
|
55
|
+
column: d.position?.start.column,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function setH(node, hName, hProperties = {}) {
|
|
62
|
+
node.data = { ...node.data, hName, hProperties: { ...hProperties } };
|
|
63
|
+
}
|
|
64
|
+
/** Render time (on a copy): maps directives to HTML elements. */
|
|
65
|
+
export function presentDirectives(tree) {
|
|
66
|
+
visit(tree, (node) => {
|
|
67
|
+
if (node.type !== 'textDirective' &&
|
|
68
|
+
node.type !== 'leafDirective' &&
|
|
69
|
+
node.type !== 'containerDirective')
|
|
70
|
+
return;
|
|
71
|
+
const d = node;
|
|
72
|
+
const attrs = d.attributes ?? {};
|
|
73
|
+
if (d.type === 'containerDirective') {
|
|
74
|
+
const label = isLabel(d.children[0]) ? d.children.shift() : undefined;
|
|
75
|
+
const title = attrs.title ?? (label ? mdToString(label) : undefined);
|
|
76
|
+
if (CALLOUTS.includes(d.name)) {
|
|
77
|
+
setH(d, 'div', { className: ['mm-callout', `mm-callout-${d.name}`], role: 'note' });
|
|
78
|
+
const heading = title ?? d.name.charAt(0).toUpperCase() + d.name.slice(1);
|
|
79
|
+
d.children.unshift({
|
|
80
|
+
type: 'paragraph',
|
|
81
|
+
children: label && !attrs.title ? label.children : [{ type: 'text', value: heading }],
|
|
82
|
+
data: { hProperties: { className: ['mm-callout-title'] } },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else if (d.name === 'details') {
|
|
86
|
+
setH(d, 'details', attrs.open !== undefined ? { open: true } : {});
|
|
87
|
+
d.children.unshift({
|
|
88
|
+
type: 'paragraph',
|
|
89
|
+
children: label ? label.children : [{ type: 'text', value: title ?? 'Details' }],
|
|
90
|
+
data: { hName: 'summary' },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else if (d.name === 'figure') {
|
|
94
|
+
setH(d, 'figure', { className: ['mm-figure'] });
|
|
95
|
+
if (label)
|
|
96
|
+
d.children.push({
|
|
97
|
+
type: 'paragraph',
|
|
98
|
+
children: label.children,
|
|
99
|
+
data: { hName: 'figcaption' },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else if (d.name === 'columns')
|
|
103
|
+
setH(d, 'div', { className: ['mm-columns'] });
|
|
104
|
+
else if (d.name === 'column')
|
|
105
|
+
setH(d, 'div', { className: ['mm-column'] });
|
|
106
|
+
else
|
|
107
|
+
setH(d, 'div', { className: ['mm-directive', `mm-directive-${d.name}`] });
|
|
108
|
+
}
|
|
109
|
+
else if (d.type === 'textDirective') {
|
|
110
|
+
if (d.name === 'kbd' || d.name === 'mark' || d.name === 'sub' || d.name === 'sup')
|
|
111
|
+
setH(d, d.name);
|
|
112
|
+
else if (d.name === 'abbr')
|
|
113
|
+
setH(d, 'abbr', attrs.title ? { title: attrs.title } : {});
|
|
114
|
+
else if (d.name === 'badge')
|
|
115
|
+
setH(d, 'span', { className: ['mm-badge', `mm-badge-${attrs.type ?? 'default'}`] });
|
|
116
|
+
else
|
|
117
|
+
setH(d, 'span', { className: ['mm-directive', `mm-directive-${d.name}`] });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
if (d.name === 'br')
|
|
121
|
+
setH(d, 'br');
|
|
122
|
+
else
|
|
123
|
+
setH(d, 'div', { className: ['mm-directive', `mm-directive-${d.name}`] });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { gemoji } from 'gemoji';
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
let names;
|
|
4
|
+
function lookup() {
|
|
5
|
+
if (!names) {
|
|
6
|
+
names = new Map();
|
|
7
|
+
for (const entry of gemoji)
|
|
8
|
+
for (const name of entry.names)
|
|
9
|
+
names.set(name, entry.emoji);
|
|
10
|
+
}
|
|
11
|
+
return names;
|
|
12
|
+
}
|
|
13
|
+
const SHORTCODE = /:([a-z0-9_+-]+):/g;
|
|
14
|
+
/** Render time: replaces GitHub `:shortcode:` names with emoji. Unknown names stay as written. */
|
|
15
|
+
export function presentEmoji(tree) {
|
|
16
|
+
const map = lookup();
|
|
17
|
+
visit(tree, 'text', (node) => {
|
|
18
|
+
if (!node.value.includes(':'))
|
|
19
|
+
return;
|
|
20
|
+
node.value = node.value.replace(SHORTCODE, (whole, name) => map.get(name) ?? whole);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Root } from 'mdast';
|
|
2
|
+
import type { Diagnostic } from '../types.js';
|
|
3
|
+
/** Reads the first YAML or TOML frontmatter block. Invalid data is reported, never thrown. */
|
|
4
|
+
export declare function readFrontmatter(tree: Root, report: (d: Diagnostic) => void): Record<string, unknown> | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { parse as parseToml } from 'smol-toml';
|
|
2
|
+
import { parse as parseYaml } from 'yaml';
|
|
3
|
+
/** Reads the first YAML or TOML frontmatter block. Invalid data is reported, never thrown. */
|
|
4
|
+
export function readFrontmatter(tree, report) {
|
|
5
|
+
const node = tree.children[0];
|
|
6
|
+
if (!node || (node.type !== 'yaml' && node.type !== 'toml'))
|
|
7
|
+
return undefined;
|
|
8
|
+
const value = node.value;
|
|
9
|
+
try {
|
|
10
|
+
const data = node.type === 'yaml' ? parseYaml(value) : parseToml(value);
|
|
11
|
+
if (data === null || data === undefined)
|
|
12
|
+
return {};
|
|
13
|
+
if (typeof data !== 'object' || Array.isArray(data)) {
|
|
14
|
+
report({
|
|
15
|
+
code: 'MM010',
|
|
16
|
+
rule: 'invalid-frontmatter',
|
|
17
|
+
severity: 'error',
|
|
18
|
+
message: 'Frontmatter must be a mapping of keys to values',
|
|
19
|
+
line: node.position?.start.line,
|
|
20
|
+
column: 1,
|
|
21
|
+
});
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
report({
|
|
28
|
+
code: 'MM010',
|
|
29
|
+
rule: 'invalid-frontmatter',
|
|
30
|
+
severity: 'error',
|
|
31
|
+
message: `Invalid ${node.type.toUpperCase()} frontmatter: ${error.message.split('\n')[0]}`,
|
|
32
|
+
line: node.position?.start.line,
|
|
33
|
+
column: 1,
|
|
34
|
+
});
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Heading, Root } from 'mdast';
|
|
2
|
+
import type { Diagnostic } from '../types.js';
|
|
3
|
+
export declare function headingId(node: Heading): string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Gives every heading a GitHub-compatible id in `data.hProperties.id`.
|
|
6
|
+
* A trailing `{#custom-id}` sets the id explicitly; the marker is kept in `data.customId` so
|
|
7
|
+
* serialization can write it back.
|
|
8
|
+
*/
|
|
9
|
+
export declare function addHeadingIds(tree: Root, report: (d: Diagnostic) => void): void;
|
|
10
|
+
export interface OutlineItem {
|
|
11
|
+
depth: number;
|
|
12
|
+
text: string;
|
|
13
|
+
id: string;
|
|
14
|
+
line?: number;
|
|
15
|
+
children: OutlineItem[];
|
|
16
|
+
}
|
|
17
|
+
/** The heading tree of a document. */
|
|
18
|
+
export declare function outline(tree: Root): OutlineItem[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import GithubSlugger from 'github-slugger';
|
|
2
|
+
import { toString as mdToString } from 'mdast-util-to-string';
|
|
3
|
+
import { visit } from 'unist-util-visit';
|
|
4
|
+
const CUSTOM_ID = /\s*\{#([A-Za-z][\w:.-]*)\}\s*$/;
|
|
5
|
+
export function headingId(node) {
|
|
6
|
+
const id = node.data?.hProperties?.id;
|
|
7
|
+
return typeof id === 'string' ? id : undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Gives every heading a GitHub-compatible id in `data.hProperties.id`.
|
|
11
|
+
* A trailing `{#custom-id}` sets the id explicitly; the marker is kept in `data.customId` so
|
|
12
|
+
* serialization can write it back.
|
|
13
|
+
*/
|
|
14
|
+
export function addHeadingIds(tree, report) {
|
|
15
|
+
const slugger = new GithubSlugger();
|
|
16
|
+
const seen = new Map();
|
|
17
|
+
visit(tree, 'heading', (node) => {
|
|
18
|
+
let id;
|
|
19
|
+
const last = node.children[node.children.length - 1];
|
|
20
|
+
if (last && last.type === 'text') {
|
|
21
|
+
const m = CUSTOM_ID.exec(last.value);
|
|
22
|
+
if (m?.[1]) {
|
|
23
|
+
id = m[1];
|
|
24
|
+
last.value = last.value.slice(0, m.index);
|
|
25
|
+
if (last.value === '')
|
|
26
|
+
node.children.pop();
|
|
27
|
+
node.data = { ...node.data, customId: id };
|
|
28
|
+
slugger.slug(id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
id ??= slugger.slug(mdToString(node));
|
|
32
|
+
if (seen.has(id)) {
|
|
33
|
+
report({
|
|
34
|
+
code: 'MM003',
|
|
35
|
+
rule: 'duplicate-heading-id',
|
|
36
|
+
severity: 'warning',
|
|
37
|
+
message: `Heading id "${id}" is already used on line ${seen.get(id)}`,
|
|
38
|
+
line: node.position?.start.line,
|
|
39
|
+
column: node.position?.start.column,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
seen.set(id, node.position?.start.line ?? 0);
|
|
44
|
+
node.data = { ...node.data, hProperties: { ...node.data?.hProperties, id } };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** The heading tree of a document. */
|
|
48
|
+
export function outline(tree) {
|
|
49
|
+
const roots = [];
|
|
50
|
+
const stack = [];
|
|
51
|
+
visit(tree, 'heading', (node) => {
|
|
52
|
+
const item = {
|
|
53
|
+
depth: node.depth,
|
|
54
|
+
text: mdToString(node),
|
|
55
|
+
id: headingId(node) ?? '',
|
|
56
|
+
line: node.position?.start.line,
|
|
57
|
+
children: [],
|
|
58
|
+
};
|
|
59
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= item.depth)
|
|
60
|
+
stack.pop();
|
|
61
|
+
const parent = stack[stack.length - 1];
|
|
62
|
+
if (parent)
|
|
63
|
+
parent.children.push(item);
|
|
64
|
+
else
|
|
65
|
+
roots.push(item);
|
|
66
|
+
stack.push(item);
|
|
67
|
+
});
|
|
68
|
+
return roots;
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { List, Parent, Root } from 'mdast';
|
|
2
|
+
import type { TocOptions } from '../types.js';
|
|
3
|
+
/** mdast node holding a generated table of contents. Serialized between `<!-- toc -->` markers. */
|
|
4
|
+
export interface TocNode extends Parent {
|
|
5
|
+
type: 'toc';
|
|
6
|
+
children: List[];
|
|
7
|
+
}
|
|
8
|
+
declare module 'mdast' {
|
|
9
|
+
interface RootContentMap {
|
|
10
|
+
toc: TocNode;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export declare function buildToc(tree: Root, opts?: TocOptions): TocNode;
|
|
14
|
+
/**
|
|
15
|
+
* Replaces `[[toc]]`, `::toc` and `<!-- toc --> … <!-- tocstop -->` with a generated TOC.
|
|
16
|
+
* Returns true when a TOC was placed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function insertToc(tree: Root, opts?: TocOptions): boolean;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { toString as mdToString } from 'mdast-util-to-string';
|
|
2
|
+
import { outline } from './headings.js';
|
|
3
|
+
const OPEN = /^<!--\s*toc\s*-->$/i;
|
|
4
|
+
const CLOSE = /^<!--\s*(?:tocstop|\/toc)\s*-->$/i;
|
|
5
|
+
const INLINE_MARKER = /^\[\[?toc\]\]?$/i;
|
|
6
|
+
function isMarker(node) {
|
|
7
|
+
if (node.type === 'paragraph')
|
|
8
|
+
return INLINE_MARKER.test(mdToString(node).trim());
|
|
9
|
+
if (node.type === 'leafDirective')
|
|
10
|
+
return node.name === 'toc';
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
function flatten(items, min, max) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const item of items) {
|
|
16
|
+
if (item.depth >= min && item.depth <= max) {
|
|
17
|
+
out.push({ ...item, children: flatten(item.children, min, max) });
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
out.push(...flatten(item.children, min, max));
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function toList(items) {
|
|
25
|
+
return {
|
|
26
|
+
type: 'list',
|
|
27
|
+
ordered: false,
|
|
28
|
+
spread: false,
|
|
29
|
+
children: items.map((item) => ({
|
|
30
|
+
type: 'listItem',
|
|
31
|
+
spread: false,
|
|
32
|
+
children: [
|
|
33
|
+
{
|
|
34
|
+
type: 'paragraph',
|
|
35
|
+
children: [
|
|
36
|
+
{
|
|
37
|
+
type: 'link',
|
|
38
|
+
url: `#${item.id}`,
|
|
39
|
+
children: [{ type: 'text', value: item.text }],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
...(item.children.length > 0 ? [toList(item.children)] : []),
|
|
44
|
+
],
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function buildToc(tree, opts = {}) {
|
|
49
|
+
const min = opts.minDepth ?? 2;
|
|
50
|
+
const max = opts.maxDepth ?? 3;
|
|
51
|
+
return {
|
|
52
|
+
type: 'toc',
|
|
53
|
+
children: [toList(flatten(outline(tree), min, max))],
|
|
54
|
+
data: {
|
|
55
|
+
hName: 'nav',
|
|
56
|
+
hProperties: { className: ['mm-toc'], ariaLabel: opts.title ?? 'Table of contents' },
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Replaces `[[toc]]`, `::toc` and `<!-- toc --> … <!-- tocstop -->` with a generated TOC.
|
|
62
|
+
* Returns true when a TOC was placed.
|
|
63
|
+
*/
|
|
64
|
+
export function insertToc(tree, opts = {}) {
|
|
65
|
+
const kids = tree.children;
|
|
66
|
+
let placed = false;
|
|
67
|
+
for (let i = 0; i < kids.length; i++) {
|
|
68
|
+
const node = kids[i];
|
|
69
|
+
if (node.type === 'html' && OPEN.test(node.value.trim())) {
|
|
70
|
+
let end = i + 1;
|
|
71
|
+
while (end < kids.length) {
|
|
72
|
+
const n = kids[end];
|
|
73
|
+
if (n.type === 'html' && CLOSE.test(n.value.trim()))
|
|
74
|
+
break;
|
|
75
|
+
end++;
|
|
76
|
+
}
|
|
77
|
+
const removeTo = end < kids.length ? end + 1 : i + 1;
|
|
78
|
+
kids.splice(i, removeTo - i, buildToc(tree, opts));
|
|
79
|
+
placed = true;
|
|
80
|
+
}
|
|
81
|
+
else if (isMarker(node)) {
|
|
82
|
+
kids.splice(i, 1, buildToc(tree, opts));
|
|
83
|
+
placed = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const empty = (buildToc(tree, opts).children[0]?.children.length ?? 0) === 0;
|
|
87
|
+
if (!placed && !empty && opts.position && opts.position !== 'marker-only') {
|
|
88
|
+
let at = 0;
|
|
89
|
+
if (opts.position === 'after-title') {
|
|
90
|
+
const h1 = kids.findIndex((n) => n.type === 'heading' && n.depth === 1);
|
|
91
|
+
at = h1 >= 0 ? h1 + 1 : kids.findIndex((n) => n.type !== 'yaml' && n.type !== 'toml');
|
|
92
|
+
if (at < 0)
|
|
93
|
+
at = kids.length;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const fm = kids.findIndex((n) => n.type !== 'yaml' && n.type !== 'toml');
|
|
97
|
+
at = fm < 0 ? kids.length : fm;
|
|
98
|
+
}
|
|
99
|
+
kids.splice(at, 0, buildToc(tree, opts));
|
|
100
|
+
placed = true;
|
|
101
|
+
}
|
|
102
|
+
return placed;
|
|
103
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Root } from 'mdast';
|
|
2
|
+
import { type SerializeOptions } from './serialize.js';
|
|
3
|
+
import type { Diagnostic, MarkupMeDocument, MarkupMePlugin, ParseOptions, ProcessorOptions, RenderOptions, RenderResult, Rule, TocOptions, ValidateOptions } from './types.js';
|
|
4
|
+
export type BuiltinTransform = 'toc' | 'normalize';
|
|
5
|
+
export interface TransformOptions {
|
|
6
|
+
/** Built-in transforms by name, or plugins. Applied in order. */
|
|
7
|
+
plugins?: (BuiltinTransform | MarkupMePlugin)[];
|
|
8
|
+
toc?: TocOptions;
|
|
9
|
+
}
|
|
10
|
+
export interface TransformResult {
|
|
11
|
+
document: MarkupMeDocument;
|
|
12
|
+
diagnostics: Diagnostic[];
|
|
13
|
+
}
|
|
14
|
+
export interface Processor {
|
|
15
|
+
readonly options: Readonly<ProcessorOptions>;
|
|
16
|
+
parse(source: string, opts?: ParseOptions): MarkupMeDocument;
|
|
17
|
+
transform(doc: MarkupMeDocument, opts?: TransformOptions): TransformResult;
|
|
18
|
+
render(input: MarkupMeDocument | string, opts?: RenderOptions): Promise<RenderResult>;
|
|
19
|
+
serialize(input: MarkupMeDocument | Root, opts?: SerializeOptions): string;
|
|
20
|
+
validate(input: MarkupMeDocument | string, opts?: ValidateOptions): Diagnostic[];
|
|
21
|
+
/** Checks whether serializing and re-parsing keeps the tree. Reports what would change. */
|
|
22
|
+
roundTrip(doc: MarkupMeDocument): {
|
|
23
|
+
lossless: boolean;
|
|
24
|
+
markdown: string;
|
|
25
|
+
diagnostics: Diagnostic[];
|
|
26
|
+
};
|
|
27
|
+
/** The rules `validate` runs (built-in plus plugin rules). */
|
|
28
|
+
rules(): Rule[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates an isolated processor. Options and plugins are fixed at creation; there is no global
|
|
32
|
+
* registry, so two processors never affect each other.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createProcessor(options?: ProcessorOptions): Processor;
|
|
35
|
+
/** Stable error with a code. */
|
|
36
|
+
export declare class MarkupMeError extends Error {
|
|
37
|
+
readonly code: string;
|
|
38
|
+
constructor(code: string, message: string);
|
|
39
|
+
}
|
|
40
|
+
/** Parses with default options. */
|
|
41
|
+
export declare const parse: (source: string, opts?: ParseOptions) => MarkupMeDocument;
|
|
42
|
+
/** Renders with default options (HTML, sanitized). */
|
|
43
|
+
export declare const render: (input: MarkupMeDocument | string, opts?: RenderOptions) => Promise<RenderResult>;
|
|
44
|
+
export declare const transform: (doc: MarkupMeDocument, opts?: TransformOptions) => TransformResult;
|
|
45
|
+
export declare const serialize: (input: MarkupMeDocument | Root, opts?: SerializeOptions) => string;
|
|
46
|
+
export declare const validate: (input: MarkupMeDocument | string, opts?: ValidateOptions) => Diagnostic[];
|