@markupme/core 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 (45) hide show
  1. package/THIRD-PARTY-NOTICES.md +37 -0
  2. package/dist/format.d.ts +3 -0
  3. package/dist/format.js +5 -0
  4. package/dist/index.d.ts +12 -0
  5. package/dist/index.js +11 -0
  6. package/dist/parse.d.ts +6 -0
  7. package/dist/parse.js +66 -0
  8. package/dist/plugins/alerts.d.ts +8 -0
  9. package/dist/plugins/alerts.js +50 -0
  10. package/dist/plugins/directives.d.ts +16 -0
  11. package/dist/plugins/directives.js +126 -0
  12. package/dist/plugins/emoji.d.ts +3 -0
  13. package/dist/plugins/emoji.js +22 -0
  14. package/dist/plugins/frontmatter.d.ts +4 -0
  15. package/dist/plugins/frontmatter.js +37 -0
  16. package/dist/plugins/headings.d.ts +18 -0
  17. package/dist/plugins/headings.js +69 -0
  18. package/dist/plugins/math-syntax.d.ts +20 -0
  19. package/dist/plugins/math-syntax.js +279 -0
  20. package/dist/plugins/math.d.ts +2 -0
  21. package/dist/plugins/math.js +17 -0
  22. package/dist/plugins/toc.d.ts +18 -0
  23. package/dist/plugins/toc.js +103 -0
  24. package/dist/processor.d.ts +46 -0
  25. package/dist/processor.js +127 -0
  26. package/dist/render/html.d.ts +10 -0
  27. package/dist/render/html.js +217 -0
  28. package/dist/render/katex.d.ts +13 -0
  29. package/dist/render/katex.js +79 -0
  30. package/dist/render/schema.d.ts +7 -0
  31. package/dist/render/schema.js +51 -0
  32. package/dist/render/text.d.ts +3 -0
  33. package/dist/render/text.js +48 -0
  34. package/dist/serialize.d.ts +16 -0
  35. package/dist/serialize.js +51 -0
  36. package/dist/types.d.ts +170 -0
  37. package/dist/types.js +2 -0
  38. package/dist/util.d.ts +48 -0
  39. package/dist/util.js +162 -0
  40. package/dist/validate.d.ts +5 -0
  41. package/dist/validate.js +209 -0
  42. package/dist/version.d.ts +1 -0
  43. package/dist/version.js +1 -0
  44. package/package.json +82 -4
  45. package/README.md +0 -3
@@ -0,0 +1,37 @@
1
+ # Third-party notices
2
+
3
+ `@markupme/core` contains code ported to TypeScript from these MIT-licensed projects:
4
+
5
+ | File | Ported from | Copyright |
6
+ | ---- | ----------- | --------- |
7
+ | `src/render/katex.ts` | [rehype-katex](https://github.com/remarkjs/remark-math/tree/main/packages/rehype-katex) 7.0.1 | Copyright (c) Junyoung Choi <fluke8259@gmail.com> |
8
+ | `src/plugins/math.ts` | [remark-math](https://github.com/remarkjs/remark-math/tree/main/packages/remark-math) 6.0.0 | Copyright (c) Junyoung Choi <fluke8259@gmail.com> |
9
+ | `src/plugins/math-syntax.ts` | [micromark-extension-math](https://github.com/micromark/micromark-extension-math) 3.1.0 | Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com> |
10
+
11
+ All three are distributed under the MIT License:
12
+
13
+ ```text
14
+ (The MIT License)
15
+
16
+ Copyright (c) Junyoung Choi <fluke8259@gmail.com>
17
+ Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining
20
+ a copy of this software and associated documentation files (the
21
+ 'Software'), to deal in the Software without restriction, including
22
+ without limitation the rights to use, copy, modify, merge, publish,
23
+ distribute, sublicense, and/or sell copies of the Software, and to
24
+ permit persons to whom the Software is furnished to do so, subject to
25
+ the following conditions:
26
+
27
+ The above copyright notice and this permission notice shall be
28
+ included in all copies or substantial portions of the Software.
29
+
30
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
31
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
32
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
33
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
34
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
35
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
36
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37
+ ```
@@ -0,0 +1,3 @@
1
+ import type { Diagnostic } from './types.js';
2
+ /** `file:line:col severity code message (rule)`: the format editors and CI annotate from. */
3
+ export declare function formatDiagnostic(d: Diagnostic): string;
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
+ }
@@ -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';
@@ -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 remarkParse from 'remark-parse';
5
+ import { unified } from 'unified';
6
+ import { annotateAlerts } from './plugins/alerts.js';
7
+ import { normalizeDirectives } from './plugins/directives.js';
8
+ import { readFrontmatter } from './plugins/frontmatter.js';
9
+ import { addHeadingIds } from './plugins/headings.js';
10
+ import { remarkMath } from './plugins/math.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,3 @@
1
+ import type { Root } from 'mdast';
2
+ /** Render time: replaces GitHub `:shortcode:` names with emoji. Unknown names stay as written. */
3
+ export declare function presentEmoji(tree: Root): void;
@@ -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,20 @@
1
+ import type { Extension } from 'micromark-util-types';
2
+ declare module 'micromark-util-types' {
3
+ interface TokenTypeMap {
4
+ mathFlow: 'mathFlow';
5
+ mathFlowFence: 'mathFlowFence';
6
+ mathFlowFenceMeta: 'mathFlowFenceMeta';
7
+ mathFlowFenceSequence: 'mathFlowFenceSequence';
8
+ mathFlowValue: 'mathFlowValue';
9
+ mathText: 'mathText';
10
+ mathTextData: 'mathTextData';
11
+ mathTextPadding: 'mathTextPadding';
12
+ mathTextSequence: 'mathTextSequence';
13
+ }
14
+ }
15
+ export interface MathSyntaxOptions {
16
+ /** Whether `$x$` (one dollar) is text math. Default `true`. */
17
+ singleDollarTextMath?: boolean | null | undefined;
18
+ }
19
+ /** micromark extension for math syntax. */
20
+ export declare function mathSyntax(options?: MathSyntaxOptions | null): Extension;