markdown2typst 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/LICENSE +22 -0
- package/README.md +232 -0
- package/dist/markdown2typst.js +20479 -0
- package/dist/markdown2typst.js.map +7 -0
- package/dist/markdown2typst.min.js +103 -0
- package/package.json +74 -0
- package/src/block-renderer.ts +363 -0
- package/src/collectors.ts +138 -0
- package/src/frontmatter.ts +197 -0
- package/src/inline-renderer.ts +266 -0
- package/src/markdown2typst.ts +96 -0
- package/src/old/oldmarkdown2typst.ts +1092 -0
- package/src/output-builder.ts +229 -0
- package/src/parser.ts +53 -0
- package/src/types.ts +156 -0
- package/src/utils.ts +241 -0
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "markdown2typst",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A comprehensive JavaScript library for converting Markdown to Typst code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/markdown2typst.js",
|
|
7
|
+
"types": "./src/markdown2typst.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/markdown2typst.ts",
|
|
11
|
+
"import": "./dist/markdown2typst.js",
|
|
12
|
+
"default": "./dist/markdown2typst.min.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"markdown",
|
|
23
|
+
"typst",
|
|
24
|
+
"converter",
|
|
25
|
+
"parser",
|
|
26
|
+
"markdown-to-typst",
|
|
27
|
+
"markdown-2-typst",
|
|
28
|
+
"markdown2typst",
|
|
29
|
+
"gfm",
|
|
30
|
+
"github-flavored-markdown",
|
|
31
|
+
"remark",
|
|
32
|
+
"unified",
|
|
33
|
+
"documentation"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "node build.js",
|
|
37
|
+
"build:dev": "node build.js --dev",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "node --test test/validate.js"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/Mapaor/markdown2typst.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/Mapaor/markdown2typst/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/Mapaor/markdown2typst#readme",
|
|
49
|
+
"author": "Martí Pardo <pardo.marti@gmail.com>",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"country-list": "^2.4.1",
|
|
53
|
+
"js-yaml": "^4.1.1",
|
|
54
|
+
"langs": "^2.0.0",
|
|
55
|
+
"remark-frontmatter": "^5.0.0",
|
|
56
|
+
"remark-gfm": "^4.0.0",
|
|
57
|
+
"remark-math": "^6.0.0",
|
|
58
|
+
"remark-parse": "^11.0.0",
|
|
59
|
+
"tex2typst": "^0.5.5",
|
|
60
|
+
"unified": "^11.0.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/country-list": "^2.1.4",
|
|
64
|
+
"@types/js-yaml": "^4.0.9",
|
|
65
|
+
"@types/langs": "^2.0.5",
|
|
66
|
+
"@types/mdast": "^4.0.4",
|
|
67
|
+
"@types/node": "^25.0.8",
|
|
68
|
+
"esbuild": "^0.27.2",
|
|
69
|
+
"typescript": "^5.3.0"
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"node": ">=18"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block node rendering functions
|
|
3
|
+
* @module block-renderer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { tex2typst } from 'tex2typst';
|
|
7
|
+
import type {
|
|
8
|
+
Content,
|
|
9
|
+
Heading,
|
|
10
|
+
Paragraph,
|
|
11
|
+
List,
|
|
12
|
+
ListItem,
|
|
13
|
+
Code,
|
|
14
|
+
Blockquote,
|
|
15
|
+
Table,
|
|
16
|
+
TableRow,
|
|
17
|
+
TableCell
|
|
18
|
+
} from 'mdast';
|
|
19
|
+
import type { MathNode, RenderContext } from './types.js';
|
|
20
|
+
import { renderInlines } from './inline-renderer.js';
|
|
21
|
+
import { indentLines, isNonEmpty, plainTextFromPhrasing, maxBacktickRun } from './utils.js';
|
|
22
|
+
import { ErrorSeverity } from './types.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render a block-level MDAST node to Typst markup.
|
|
26
|
+
* Dispatches to appropriate renderer based on node type.
|
|
27
|
+
*
|
|
28
|
+
* @param node - Block-level content node
|
|
29
|
+
* @param indentLevel - Current indentation level
|
|
30
|
+
* @param context - Rendering context with definitions and footnotes
|
|
31
|
+
* @returns Rendered Typst string or null if node should be skipped
|
|
32
|
+
*/
|
|
33
|
+
export function renderBlock(
|
|
34
|
+
node: Content,
|
|
35
|
+
indentLevel: number,
|
|
36
|
+
context: RenderContext
|
|
37
|
+
): string | null {
|
|
38
|
+
try {
|
|
39
|
+
switch (node.type) {
|
|
40
|
+
case 'yaml':
|
|
41
|
+
case 'definition':
|
|
42
|
+
case 'footnoteDefinition':
|
|
43
|
+
return null;
|
|
44
|
+
case 'heading':
|
|
45
|
+
return renderHeading(node as Heading, indentLevel, context);
|
|
46
|
+
case 'paragraph':
|
|
47
|
+
return indentLines(
|
|
48
|
+
renderParagraph(node as Paragraph, context),
|
|
49
|
+
indentLevel
|
|
50
|
+
);
|
|
51
|
+
case 'list':
|
|
52
|
+
return renderList(node as List, indentLevel, context);
|
|
53
|
+
case 'code':
|
|
54
|
+
return renderCodeBlock(node as Code, indentLevel);
|
|
55
|
+
case 'blockquote':
|
|
56
|
+
return renderBlockquote(node as Blockquote, indentLevel, context);
|
|
57
|
+
case 'thematicBreak':
|
|
58
|
+
return indentLines('#line(length: 100%, stroke: 0.6pt)', indentLevel);
|
|
59
|
+
case 'table':
|
|
60
|
+
return renderTable(node as Table, indentLevel, context);
|
|
61
|
+
case 'math':
|
|
62
|
+
return renderMathBlock(node as MathNode, indentLevel, context);
|
|
63
|
+
default:
|
|
64
|
+
if (context.onError) {
|
|
65
|
+
context.onError({
|
|
66
|
+
severity: ErrorSeverity.WARNING,
|
|
67
|
+
message: `Unknown block node type: ${node.type}`,
|
|
68
|
+
context: 'block rendering',
|
|
69
|
+
details: { nodeType: node.type }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
76
|
+
if (context.onError) {
|
|
77
|
+
context.onError({
|
|
78
|
+
severity: ErrorSeverity.ERROR,
|
|
79
|
+
message: `Error rendering block node: ${errorMessage}`,
|
|
80
|
+
context: 'block rendering',
|
|
81
|
+
originalError: error,
|
|
82
|
+
details: { nodeType: node.type }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Return empty string to continue rendering
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render a block-level math equation to Typst.
|
|
92
|
+
* Converts LaTeX math syntax to Typst math using tex2typst.
|
|
93
|
+
*
|
|
94
|
+
* @param node - Math node
|
|
95
|
+
* @param indentLevel - Current indentation level
|
|
96
|
+
* @param context - Rendering context for error handling
|
|
97
|
+
* @returns Rendered Typst math block
|
|
98
|
+
*/
|
|
99
|
+
function renderMathBlock(node: MathNode, indentLevel: number, context: RenderContext): string {
|
|
100
|
+
// Convert LaTeX to Typst math syntax
|
|
101
|
+
const value = node.value.trim();
|
|
102
|
+
try {
|
|
103
|
+
const typstMath = tex2typst(value);
|
|
104
|
+
return indentLines(`$ ${typstMath} $`, indentLevel);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Fallback: use original LaTeX if conversion fails
|
|
107
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
108
|
+
if (context.onError) {
|
|
109
|
+
context.onError({
|
|
110
|
+
severity: ErrorSeverity.WARNING,
|
|
111
|
+
message: `Failed to convert block math LaTeX to Typst: ${errorMessage}`,
|
|
112
|
+
context: 'block math rendering',
|
|
113
|
+
originalError: error,
|
|
114
|
+
details: { latex: value }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return indentLines(`$ ${value} $`, indentLevel);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render a heading to Typst markup.
|
|
123
|
+
* Converts Markdown heading syntax (# ## ###) to Typst (= == ===).
|
|
124
|
+
*
|
|
125
|
+
* @param node - Heading node
|
|
126
|
+
* @param indentLevel - Current indentation level
|
|
127
|
+
* @param context - Rendering context with definitions and footnotes
|
|
128
|
+
* @returns Rendered Typst heading
|
|
129
|
+
*/
|
|
130
|
+
function renderHeading(
|
|
131
|
+
node: Heading,
|
|
132
|
+
indentLevel: number,
|
|
133
|
+
context: RenderContext
|
|
134
|
+
): string {
|
|
135
|
+
const level = Math.min(Math.max(node.depth, 1), 6);
|
|
136
|
+
return indentLines(
|
|
137
|
+
`${'='.repeat(level)} ${renderInlines(node.children, context)}`,
|
|
138
|
+
indentLevel
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Render a paragraph to Typst.
|
|
144
|
+
* Handles special cases like [toc] for table of contents.
|
|
145
|
+
*
|
|
146
|
+
* @param node - Paragraph node
|
|
147
|
+
* @param context - Rendering context with definitions and footnotes
|
|
148
|
+
* @returns Rendered Typst paragraph
|
|
149
|
+
*/
|
|
150
|
+
function renderParagraph(
|
|
151
|
+
node: Paragraph,
|
|
152
|
+
context: RenderContext
|
|
153
|
+
): string {
|
|
154
|
+
// Check for [toc]
|
|
155
|
+
const text = plainTextFromPhrasing(node.children, context.definitions).trim().toLowerCase();
|
|
156
|
+
if (text === '[toc]') {
|
|
157
|
+
return `#outline(title: auto, indent: auto)`;
|
|
158
|
+
}
|
|
159
|
+
return renderInlines(node.children, context);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Render a list to Typst markup.
|
|
164
|
+
*
|
|
165
|
+
* @param node - List node
|
|
166
|
+
* @param indentLevel - Current indentation level
|
|
167
|
+
* @param context - Rendering context with definitions and footnotes
|
|
168
|
+
* @returns Rendered Typst list
|
|
169
|
+
*/
|
|
170
|
+
function renderList(
|
|
171
|
+
node: List,
|
|
172
|
+
indentLevel: number,
|
|
173
|
+
context: RenderContext
|
|
174
|
+
): string {
|
|
175
|
+
const marker = node.ordered ? '+' : '-';
|
|
176
|
+
return node.children
|
|
177
|
+
.map((item) => renderListItem(item, marker, indentLevel, context))
|
|
178
|
+
.filter(isNonEmpty)
|
|
179
|
+
.join('\n');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Render a list item to Typst markup.
|
|
184
|
+
*
|
|
185
|
+
* @param node - ListItem node
|
|
186
|
+
* @param marker - List marker ('+' for ordered, '-' for unordered)
|
|
187
|
+
* @param indentLevel - Current indentation level
|
|
188
|
+
* @param context - Rendering context with definitions and footnotes
|
|
189
|
+
* @returns Rendered Typst list item
|
|
190
|
+
*/
|
|
191
|
+
function renderListItem(
|
|
192
|
+
node: ListItem,
|
|
193
|
+
marker: string,
|
|
194
|
+
indentLevel: number,
|
|
195
|
+
context: RenderContext
|
|
196
|
+
): string {
|
|
197
|
+
const baseIndent = ' '.repeat(indentLevel);
|
|
198
|
+
const nestedIndentLevel = indentLevel + 1;
|
|
199
|
+
|
|
200
|
+
const first = node.children[0];
|
|
201
|
+
const lines: string[] = [];
|
|
202
|
+
|
|
203
|
+
if (first?.type === 'paragraph') {
|
|
204
|
+
lines.push(
|
|
205
|
+
`${baseIndent}${marker} ${renderParagraph(first as Paragraph, context)}`
|
|
206
|
+
);
|
|
207
|
+
for (const child of node.children.slice(1)) {
|
|
208
|
+
if (child.type === 'list') {
|
|
209
|
+
lines.push(renderList(child as List, nestedIndentLevel, context));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const rendered = renderBlock(child as Content, nestedIndentLevel, context);
|
|
213
|
+
if (rendered) lines.push(rendered);
|
|
214
|
+
}
|
|
215
|
+
return lines.join('\n');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
lines.push(`${baseIndent}${marker}`);
|
|
219
|
+
for (const child of node.children) {
|
|
220
|
+
if (child.type === 'list') {
|
|
221
|
+
lines.push(renderList(child as List, nestedIndentLevel, context));
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const rendered = renderBlock(child as Content, nestedIndentLevel, context);
|
|
225
|
+
if (rendered) lines.push(rendered);
|
|
226
|
+
}
|
|
227
|
+
return lines.join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Render a code block to Typst markup.
|
|
232
|
+
*
|
|
233
|
+
* @param node - Code node
|
|
234
|
+
* @param indentLevel - Current indentation level
|
|
235
|
+
* @returns Rendered Typst code block
|
|
236
|
+
*/
|
|
237
|
+
function renderCodeBlock(node: Code, indentLevel: number): string {
|
|
238
|
+
const info = node.lang?.trim() ? node.lang.trim() : '';
|
|
239
|
+
const value = node.value.replace(/\n$/, '');
|
|
240
|
+
const fence = '`'.repeat(Math.max(3, maxBacktickRun(value) + 1));
|
|
241
|
+
const open = info ? `${fence}${info}` : fence;
|
|
242
|
+
const indentedCode = indentLines(value, indentLevel);
|
|
243
|
+
return [indentLines(open, indentLevel), indentedCode, indentLines(fence, indentLevel)].join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Render a table to Typst markup.
|
|
248
|
+
*
|
|
249
|
+
* @param node - Table node
|
|
250
|
+
* @param indentLevel - Current indentation level
|
|
251
|
+
* @param context - Rendering context with definitions and footnotes
|
|
252
|
+
* @returns Rendered Typst table
|
|
253
|
+
*/
|
|
254
|
+
function renderTable(
|
|
255
|
+
node: Table,
|
|
256
|
+
indentLevel: number,
|
|
257
|
+
context: RenderContext
|
|
258
|
+
): string {
|
|
259
|
+
try {
|
|
260
|
+
const rows = node.children as TableRow[];
|
|
261
|
+
if (rows.length === 0) {
|
|
262
|
+
if (context.onError) {
|
|
263
|
+
context.onError({
|
|
264
|
+
severity: ErrorSeverity.WARNING,
|
|
265
|
+
message: 'Empty table found (no rows)',
|
|
266
|
+
context: 'table rendering'
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return '';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Get column count from first row
|
|
273
|
+
const headerRow = rows[0];
|
|
274
|
+
const colCount = headerRow.children.length;
|
|
275
|
+
|
|
276
|
+
if (colCount === 0) {
|
|
277
|
+
if (context.onError) {
|
|
278
|
+
context.onError({
|
|
279
|
+
severity: ErrorSeverity.WARNING,
|
|
280
|
+
message: 'Table has no columns',
|
|
281
|
+
context: 'table rendering'
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return '';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Get alignment from node.align
|
|
288
|
+
const alignMap: Record<string, string> = {
|
|
289
|
+
left: 'left',
|
|
290
|
+
right: 'right',
|
|
291
|
+
center: 'center'
|
|
292
|
+
};
|
|
293
|
+
const aligns = (node.align ?? []).map((a) => alignMap[a ?? 'left'] ?? 'left');
|
|
294
|
+
|
|
295
|
+
// Build column specification
|
|
296
|
+
const columns = Array(colCount).fill('1fr').join(', ');
|
|
297
|
+
|
|
298
|
+
// Build table content
|
|
299
|
+
const headerCells: string[] = [];
|
|
300
|
+
for (const cell of headerRow.children as TableCell[]) {
|
|
301
|
+
const content = renderInlines(cell.children, context);
|
|
302
|
+
headerCells.push(`[*${content}*]`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const dataCells: string[] = [];
|
|
306
|
+
for (let i = 1; i < rows.length; i++) {
|
|
307
|
+
const row = rows[i];
|
|
308
|
+
for (const cell of row.children as TableCell[]) {
|
|
309
|
+
const content = renderInlines(cell.children, context);
|
|
310
|
+
dataCells.push(`[${content}]`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Build align argument
|
|
315
|
+
const alignArgs = aligns.slice(0, colCount).map((a) => a).join(', ');
|
|
316
|
+
|
|
317
|
+
const lines = [
|
|
318
|
+
`#table(`,
|
|
319
|
+
` columns: (${columns}),`,
|
|
320
|
+
` align: (${alignArgs}),`,
|
|
321
|
+
` table.header(${headerCells.join(', ')}),`,
|
|
322
|
+
` ${dataCells.join(', ')}`,
|
|
323
|
+
`)`
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
return indentLines(lines.join('\n'), indentLevel);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
329
|
+
if (context.onError) {
|
|
330
|
+
context.onError({
|
|
331
|
+
severity: ErrorSeverity.ERROR,
|
|
332
|
+
message: `Error rendering table: ${errorMessage}`,
|
|
333
|
+
context: 'table rendering',
|
|
334
|
+
originalError: error
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return '';
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Render a blockquote to Typst markup.
|
|
343
|
+
*
|
|
344
|
+
* @param node - Blockquote node
|
|
345
|
+
* @param indentLevel - Current indentation level
|
|
346
|
+
* @param context - Rendering context with definitions and footnotes
|
|
347
|
+
* @returns Rendered Typst blockquote
|
|
348
|
+
*/
|
|
349
|
+
function renderBlockquote(
|
|
350
|
+
node: Blockquote,
|
|
351
|
+
indentLevel: number,
|
|
352
|
+
context: RenderContext
|
|
353
|
+
): string {
|
|
354
|
+
const body = node.children
|
|
355
|
+
.map((child) => renderBlock(child, 0, context))
|
|
356
|
+
.filter(isNonEmpty)
|
|
357
|
+
.join('\n\n');
|
|
358
|
+
|
|
359
|
+
const open = indentLines('#quote[', indentLevel);
|
|
360
|
+
if (!body.trim()) return `${open}\n${indentLines(']', indentLevel)}`;
|
|
361
|
+
|
|
362
|
+
return [open, indentLines(body, indentLevel + 1), indentLines(']', indentLevel)].join('\n');
|
|
363
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition and footnote collection pipeline stage
|
|
3
|
+
* @module collectors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Root, Definition, FootnoteDefinition, Heading } from 'mdast';
|
|
7
|
+
import { plainTextFromPhrasing, normalizeText } from './utils.js';
|
|
8
|
+
import type { ErrorCallback } from './types.js';
|
|
9
|
+
import { ErrorSeverity } from './types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Collect all link and image reference definitions from the Markdown AST.
|
|
13
|
+
* These are used to resolve reference-style links like [text][id].
|
|
14
|
+
*
|
|
15
|
+
* @param root - The root node of the MDAST tree
|
|
16
|
+
* @param onError - Optional error callback for logging collection issues
|
|
17
|
+
* @returns Map of identifier to Definition node
|
|
18
|
+
*/
|
|
19
|
+
export function collectDefinitions(root: Root, onError?: ErrorCallback): Map<string, Definition> {
|
|
20
|
+
const definitions = new Map<string, Definition>();
|
|
21
|
+
try {
|
|
22
|
+
for (const node of root.children) {
|
|
23
|
+
if (node.type !== 'definition') continue;
|
|
24
|
+
const def = node as Definition;
|
|
25
|
+
const identifier = def.identifier.toLowerCase();
|
|
26
|
+
|
|
27
|
+
// Warn if duplicate definition found
|
|
28
|
+
if (definitions.has(identifier)) {
|
|
29
|
+
if (onError) {
|
|
30
|
+
onError({
|
|
31
|
+
severity: ErrorSeverity.WARNING,
|
|
32
|
+
message: `Duplicate link/image definition found: [${def.identifier}]`,
|
|
33
|
+
context: 'definition collection',
|
|
34
|
+
details: { identifier: def.identifier, url: def.url }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
definitions.set(identifier, def);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
43
|
+
if (onError) {
|
|
44
|
+
onError({
|
|
45
|
+
severity: ErrorSeverity.ERROR,
|
|
46
|
+
message: `Error collecting definitions: ${errorMessage}`,
|
|
47
|
+
context: 'definition collection',
|
|
48
|
+
originalError: error
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Don't throw - return partial results
|
|
52
|
+
}
|
|
53
|
+
return definitions;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Collect all footnote definitions from the Markdown AST.
|
|
58
|
+
* These are rendered inline when referenced in the text.
|
|
59
|
+
*
|
|
60
|
+
* @param root - The root node of the MDAST tree
|
|
61
|
+
* @param onError - Optional error callback for logging collection issues
|
|
62
|
+
* @returns Map of identifier to FootnoteDefinition node
|
|
63
|
+
*/
|
|
64
|
+
export function collectFootnotes(root: Root, onError?: ErrorCallback): Map<string, FootnoteDefinition> {
|
|
65
|
+
const definitions = new Map<string, FootnoteDefinition>();
|
|
66
|
+
try {
|
|
67
|
+
for (const node of root.children) {
|
|
68
|
+
if (node.type !== 'footnoteDefinition') continue;
|
|
69
|
+
const def = node as FootnoteDefinition;
|
|
70
|
+
const identifier = def.identifier.toLowerCase();
|
|
71
|
+
|
|
72
|
+
// Warn if duplicate footnote found
|
|
73
|
+
if (definitions.has(identifier)) {
|
|
74
|
+
if (onError) {
|
|
75
|
+
onError({
|
|
76
|
+
severity: ErrorSeverity.WARNING,
|
|
77
|
+
message: `Duplicate footnote definition found: [^${def.identifier}]`,
|
|
78
|
+
context: 'footnote collection',
|
|
79
|
+
details: { identifier: def.identifier }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
definitions.set(identifier, def);
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
88
|
+
if (onError) {
|
|
89
|
+
onError({
|
|
90
|
+
severity: ErrorSeverity.ERROR,
|
|
91
|
+
message: `Error collecting footnotes: ${errorMessage}`,
|
|
92
|
+
context: 'footnote collection',
|
|
93
|
+
originalError: error
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// Don't throw - return partial results
|
|
97
|
+
}
|
|
98
|
+
return definitions;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Find the first H1 heading in the document (ignoring frontmatter).
|
|
103
|
+
* Used to extract document title when useH1AsTitle option is enabled.
|
|
104
|
+
*
|
|
105
|
+
* @param root - The root node of the MDAST tree
|
|
106
|
+
* @param definitions - Map of link reference definitions
|
|
107
|
+
* @param onError - Optional error callback for logging extraction issues
|
|
108
|
+
* @returns Title text and node index, or null if no H1 found
|
|
109
|
+
*/
|
|
110
|
+
export function findLeadingH1(
|
|
111
|
+
root: Root,
|
|
112
|
+
definitions: Map<string, Definition>,
|
|
113
|
+
onError?: ErrorCallback
|
|
114
|
+
): { title: string; index: number } | null {
|
|
115
|
+
try {
|
|
116
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
117
|
+
const node = root.children[i];
|
|
118
|
+
if (node.type === 'yaml' || node.type === 'definition') continue;
|
|
119
|
+
if (node.type !== 'heading') return null;
|
|
120
|
+
const heading = node as Heading;
|
|
121
|
+
if (heading.depth !== 1) return null;
|
|
122
|
+
const title = plainTextFromPhrasing(heading.children, definitions).trim();
|
|
123
|
+
return title ? { title, index: i } : null;
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
127
|
+
if (onError) {
|
|
128
|
+
onError({
|
|
129
|
+
severity: ErrorSeverity.WARNING,
|
|
130
|
+
message: `Error extracting leading H1 title: ${errorMessage}`,
|
|
131
|
+
context: 'title extraction',
|
|
132
|
+
originalError: error
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Return null on error
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|