@effected/markdown 0.2.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.
Files changed (65) hide show
  1. package/Frontmatter.js +272 -0
  2. package/FrontmatterResolver.js +273 -0
  3. package/JsonFrontmatter.js +46 -0
  4. package/LICENSE +21 -0
  5. package/Markdown.js +251 -0
  6. package/MarkdownDiagnostic.js +85 -0
  7. package/MarkdownDocument.js +248 -0
  8. package/MarkdownEdit.js +52 -0
  9. package/MarkdownFormat.js +380 -0
  10. package/MarkdownNode.js +641 -0
  11. package/MarkdownVisitor.js +88 -0
  12. package/Mdast.js +384 -0
  13. package/README.md +255 -0
  14. package/TomlFrontmatter.js +44 -0
  15. package/YamlFrontmatter.js +45 -0
  16. package/index.d.ts +2190 -0
  17. package/index.js +15 -0
  18. package/internal/blockParser.js +419 -0
  19. package/internal/blockRegistry.js +90 -0
  20. package/internal/blockTypes.js +27 -0
  21. package/internal/blocks/atxHeading.js +54 -0
  22. package/internal/blocks/blockquote.js +40 -0
  23. package/internal/blocks/code.js +90 -0
  24. package/internal/blocks/document.js +17 -0
  25. package/internal/blocks/fencedCode.js +26 -0
  26. package/internal/blocks/footnoteDefinition.js +84 -0
  27. package/internal/blocks/frontmatter.js +56 -0
  28. package/internal/blocks/htmlBlock.js +72 -0
  29. package/internal/blocks/indentedCode.js +16 -0
  30. package/internal/blocks/linkReferenceDefinition.js +72 -0
  31. package/internal/blocks/list.js +159 -0
  32. package/internal/blocks/paragraph.js +27 -0
  33. package/internal/blocks/setextHeading.js +24 -0
  34. package/internal/blocks/table.js +378 -0
  35. package/internal/blocks/taskListItem.js +36 -0
  36. package/internal/blocks/thematicBreak.js +35 -0
  37. package/internal/carriers.js +42 -0
  38. package/internal/entities.js +26 -0
  39. package/internal/entityMap.js +7 -0
  40. package/internal/htmlTags.js +18 -0
  41. package/internal/inlineNode.js +54 -0
  42. package/internal/inlineParser.js +403 -0
  43. package/internal/inlineRegistry.js +68 -0
  44. package/internal/inlines/autolink.js +36 -0
  45. package/internal/inlines/autolinkLiteral.js +374 -0
  46. package/internal/inlines/codeSpan.js +32 -0
  47. package/internal/inlines/emphasis.js +79 -0
  48. package/internal/inlines/entity.js +21 -0
  49. package/internal/inlines/escape.js +34 -0
  50. package/internal/inlines/footnoteReference.js +63 -0
  51. package/internal/inlines/lineBreak.js +25 -0
  52. package/internal/inlines/link.js +212 -0
  53. package/internal/inlines/rawHtml.js +27 -0
  54. package/internal/inlines/strikethrough.js +81 -0
  55. package/internal/inlines/text.js +22 -0
  56. package/internal/lineIndex.js +76 -0
  57. package/internal/patterns.js +26 -0
  58. package/internal/preprocess.js +58 -0
  59. package/internal/rawInline.js +57 -0
  60. package/internal/references.js +160 -0
  61. package/internal/segments.js +36 -0
  62. package/internal/stringify.js +519 -0
  63. package/internal/unescape.js +18 -0
  64. package/package.json +61 -0
  65. package/tsdoc-metadata.json +11 -0
@@ -0,0 +1,378 @@
1
+ import { Table, TableCell, TableRow } from "../../MarkdownNode.js";
2
+ import { tableCellChildren, tableRowChildren } from "../blockTypes.js";
3
+ import { ESCAPABLE } from "../unescape.js";
4
+ import { sliceWithSegments } from "../segments.js";
5
+
6
+ //#region src/internal/blocks/table.ts
7
+ /**
8
+ * Upstream's `MAX_AUTOCOMPLETED_CELLS`: the ceiling on cells the parser
9
+ * invents to pad short rows, which is what stops a wide header followed by a
10
+ * million one-character lines from allocating unboundedly.
11
+ */
12
+ const MAX_AUTOCOMPLETED_CELLS = 524288;
13
+ const reEscapable = new RegExp(`^${ESCAPABLE}`);
14
+ /** re2c's `spacechar` class: the delimiter-row grammar's idea of whitespace. */
15
+ const isSpaceChar = (char) => char === " " || char === " " || char === "\v" || char === "\f";
16
+ /** `_scan_table_cell_end`: `[|] spacechar*`, the pipe between two cells. */
17
+ const scanCellEnd = (text, from) => {
18
+ if (text.charAt(from) !== "|") return 0;
19
+ let index = from + 1;
20
+ while (isSpaceChar(text.charAt(index))) index += 1;
21
+ return index - from;
22
+ };
23
+ /**
24
+ * `_scan_table_cell`: `(escaped_char|[^|\r\n])+`.
25
+ *
26
+ * A backslash before ASCII punctuation takes the punctuation with it, which is
27
+ * the whole mechanism behind escaped pipes: `\|` is one cell character, a bare
28
+ * `|` ends the cell.
29
+ */
30
+ const scanCell = (text, from) => {
31
+ let index = from;
32
+ while (index < text.length) {
33
+ const char = text.charAt(index);
34
+ if (char === "\\" && reEscapable.test(text.charAt(index + 1))) {
35
+ index += 2;
36
+ continue;
37
+ }
38
+ if (char === "|" || char === "\r" || char === "\n") break;
39
+ index += 1;
40
+ }
41
+ return index - from;
42
+ };
43
+ /** `_scan_table_row_end`: `spacechar* newline`. */
44
+ const scanRowEnd = (text, from) => {
45
+ let index = from;
46
+ while (isSpaceChar(text.charAt(index))) index += 1;
47
+ if (text.charAt(index) === "\r") index += 1;
48
+ if (text.charAt(index) !== "\n") return 0;
49
+ return index + 1 - from;
50
+ };
51
+ /**
52
+ * `_scan_table_start`: `[|]? marker ([|] marker)* [|]? spacechar* newline`,
53
+ * where a marker is `spacechar* [:]? [-]+ [:]? spacechar*`.
54
+ *
55
+ * Hand-rolled rather than a regular expression: the grammar nests two
56
+ * unbounded whitespace runs inside a repetition, which is exactly the shape a
57
+ * backtracking engine goes exponential on, and the pathological corpus feeds
58
+ * this scanner adversarial delimiter rows.
59
+ */
60
+ const scanDelimiterRowLine = (line, from) => {
61
+ let index = from;
62
+ if (line.charAt(index) === "|") index += 1;
63
+ for (;;) {
64
+ while (isSpaceChar(line.charAt(index))) index += 1;
65
+ if (line.charAt(index) === ":") index += 1;
66
+ let dashes = 0;
67
+ while (line.charAt(index) === "-") {
68
+ index += 1;
69
+ dashes += 1;
70
+ }
71
+ if (dashes === 0) return false;
72
+ if (line.charAt(index) === ":") index += 1;
73
+ while (isSpaceChar(line.charAt(index))) index += 1;
74
+ if (line.charAt(index) !== "|") return index >= line.length;
75
+ index += 1;
76
+ let lookahead = index;
77
+ while (isSpaceChar(line.charAt(lookahead))) lookahead += 1;
78
+ if (lookahead >= line.length) return true;
79
+ }
80
+ };
81
+ /**
82
+ * Scan `source` as one table row, upstream's `row_from_string`.
83
+ *
84
+ * Returns `undefined` unless the WHOLE text is consumed and at least one cell
85
+ * came out of it — which is what makes a line either a row or not a row, with
86
+ * no partial verdict in between.
87
+ */
88
+ const rowFromSource = (source) => {
89
+ const { text } = source;
90
+ const length = text.length;
91
+ let cells = [];
92
+ let paragraphOffset = 0;
93
+ let expectMoreCells = true;
94
+ let offset = scanCellEnd(text, 0);
95
+ while (offset < length && expectMoreCells) {
96
+ const cellMatched = scanCell(text, offset);
97
+ const pipeMatched = scanCellEnd(text, offset + cellMatched);
98
+ if (cellMatched > 0 || pipeMatched > 0) {
99
+ let start = offset;
100
+ while (start > paragraphOffset && text.charAt(start - 1) !== "|") start -= 1;
101
+ cells.push({
102
+ start,
103
+ contentStart: offset,
104
+ end: offset + cellMatched
105
+ });
106
+ }
107
+ offset += cellMatched + pipeMatched;
108
+ if (pipeMatched > 0) {
109
+ expectMoreCells = true;
110
+ continue;
111
+ }
112
+ const rowEnd = scanRowEnd(text, offset);
113
+ offset += rowEnd;
114
+ if (rowEnd > 0 && offset !== length) {
115
+ paragraphOffset = offset;
116
+ cells = [];
117
+ offset += scanCellEnd(text, offset);
118
+ expectMoreCells = true;
119
+ } else expectMoreCells = false;
120
+ }
121
+ if (offset !== length || cells.length === 0) return;
122
+ return {
123
+ cells,
124
+ paragraphOffset
125
+ };
126
+ };
127
+ /**
128
+ * Cut `[from, to)` out of `source`, dropping the backslash of every `\|` in
129
+ * it — upstream's `unescape_pipes`.
130
+ *
131
+ * Upstream scans left to right and steps over the pipe it just unescaped, so
132
+ * in `\\|` the SECOND backslash is the one that escapes: this is a byte loop,
133
+ * not an understanding of which backslashes are themselves escaped. The
134
+ * lookahead stops at `to`, because past it lies the delimiter pipe that ended
135
+ * the cell and unescaping into it would eat a column boundary.
136
+ */
137
+ const cellContent = (source, from, to) => {
138
+ const { text } = source;
139
+ const drops = [];
140
+ for (let index = from; index < to; index += 1) if (text.charAt(index) === "\\" && index + 1 < to && text.charAt(index + 1) === "|") {
141
+ drops.push(index);
142
+ index += 1;
143
+ }
144
+ if (drops.length === 0) return {
145
+ text: text.slice(from, to),
146
+ segments: rebase(sliceWithSegments(source.segments, from, to), 0)
147
+ };
148
+ let content = "";
149
+ const segments = [];
150
+ let cursor = from;
151
+ for (const drop of [...drops, to]) {
152
+ if (drop > cursor) {
153
+ segments.push(...rebase(sliceWithSegments(source.segments, cursor, drop), content.length));
154
+ content += text.slice(cursor, drop);
155
+ }
156
+ cursor = drop + 1;
157
+ }
158
+ return {
159
+ text: content,
160
+ segments
161
+ };
162
+ };
163
+ /** Shift a segment run's text offsets so it sits at `at` in a longer string. */
164
+ const rebase = (segments, at) => at === 0 ? segments : segments.map((segment) => ({
165
+ textOffset: segment.textOffset + at,
166
+ sourceOffset: segment.sourceOffset,
167
+ length: segment.length
168
+ }));
169
+ /** The absolute source offset of `index` in a row source. */
170
+ const offsetAt = (source, index, fallback) => {
171
+ for (const segment of source.segments) {
172
+ if (index < segment.textOffset) return segment.sourceOffset;
173
+ if (index <= segment.textOffset + segment.length) return segment.sourceOffset + (index - segment.textOffset);
174
+ }
175
+ const last = source.segments[source.segments.length - 1];
176
+ return last === void 0 ? fallback : last.sourceOffset + last.length;
177
+ };
178
+ /**
179
+ * The alignment each delimiter cell declares: a leading colon means left, a
180
+ * trailing colon right, both center, neither nothing.
181
+ *
182
+ * Upstream reads the first and last byte of the TRIMMED cell buffer, so the
183
+ * whitespace the scanner allows around a marker never reaches this decision.
184
+ */
185
+ const alignmentsOf = (source, row) => row.cells.map((cell) => {
186
+ const text = source.text.slice(cell.contentStart, cell.end).trim();
187
+ const left = text.startsWith(":");
188
+ const right = text.endsWith(":");
189
+ if (left && right) return "center";
190
+ if (left) return "left";
191
+ if (right) return "right";
192
+ return null;
193
+ });
194
+ /** The row source for the current line, from `nextNonspace` to its end. */
195
+ const lineSource = (scanner) => {
196
+ const text = scanner.currentLine.slice(scanner.nextNonspace);
197
+ return {
198
+ text: `${text}\n`,
199
+ segments: [{
200
+ textOffset: 0,
201
+ sourceOffset: scanner.lineStart + scanner.nextNonspace,
202
+ length: text.length
203
+ }]
204
+ };
205
+ };
206
+ /** The row source for a paragraph's accumulated content. */
207
+ const paragraphSource = (block) => ({
208
+ text: block.stringContent,
209
+ segments: block.segments
210
+ });
211
+ /**
212
+ * Close `block` at `endOffset`.
213
+ *
214
+ * `finalizeBlock` is what moves the tip back up to the parent, but it dates
215
+ * the block to the end of the last line the loop finished — which for a row
216
+ * built inside a block start is the line before. The end is corrected here.
217
+ */
218
+ const closeAt = (scanner, block, endOffset) => {
219
+ scanner.finalizeBlock(block, scanner.lineNumber);
220
+ block.endLine = scanner.lineNumber;
221
+ block.endOffset = endOffset;
222
+ };
223
+ /**
224
+ * Build one row of `table` from `row`, as a `tableRow` block of `tableCell`
225
+ * children, truncated or padded to the table's column count.
226
+ */
227
+ const addRow = (scanner, data, source, row, rowStartOffset, rowEndOffset) => {
228
+ const rowBlock = scanner.addChild("tableRow", rowStartOffset - scanner.lineStart);
229
+ rowBlock.startOffset = rowStartOffset;
230
+ const used = Math.min(row.cells.length, data.columns);
231
+ for (let index = 0; index < used; index += 1) {
232
+ const cell = row.cells[index];
233
+ if (cell === void 0) continue;
234
+ const start = offsetAt(source, cell.start, rowStartOffset);
235
+ const cellBlock = scanner.addChild("tableCell", start - scanner.lineStart);
236
+ cellBlock.startOffset = start;
237
+ const content = cellContent(source, cell.contentStart, cell.end);
238
+ cellBlock.stringContent = content.text;
239
+ cellBlock.segments.push(...content.segments);
240
+ closeAt(scanner, cellBlock, offsetAt(source, cell.end, start));
241
+ }
242
+ for (let index = used; index < data.columns; index += 1) {
243
+ const cellBlock = scanner.addChild("tableCell", rowEndOffset - scanner.lineStart);
244
+ cellBlock.startOffset = rowEndOffset;
245
+ closeAt(scanner, cellBlock, rowEndOffset);
246
+ }
247
+ data.rows += 1;
248
+ data.nonemptyCells += used;
249
+ closeAt(scanner, rowBlock, rowEndOffset);
250
+ };
251
+ /**
252
+ * The header start: a delimiter row under a paragraph promotes that paragraph
253
+ * into a table.
254
+ *
255
+ * Upstream's `try_opening_table_header`.
256
+ */
257
+ const tableHeaderStart = {
258
+ name: "tableHeader",
259
+ trigger: (scanner, container) => {
260
+ if (scanner.indented || container.type !== "paragraph" || container.data.tableVisited === true) return 0;
261
+ if (!scanDelimiterRowLine(scanner.currentLine, scanner.nextNonspace)) return 0;
262
+ const delimiterSource = lineSource(scanner);
263
+ const delimiterRow = rowFromSource(delimiterSource);
264
+ if (delimiterRow === void 0) return 0;
265
+ const headerSource = paragraphSource(container);
266
+ const headerRow = rowFromSource(headerSource);
267
+ if (headerRow === void 0 || headerRow.cells.length !== delimiterRow.cells.length) {
268
+ container.data.tableVisited = true;
269
+ return 0;
270
+ }
271
+ scanner.closeUnmatchedBlocks();
272
+ const headerStart = offsetAt(headerSource, headerRow.paragraphOffset, container.startOffset);
273
+ if (headerRow.paragraphOffset > 0) {
274
+ const reclaimed = cellContent(headerSource, 0, headerRow.paragraphOffset);
275
+ const paragraph = scanner.insertBefore(container, "paragraph");
276
+ paragraph.stringContent = reclaimed.text;
277
+ paragraph.segments.push(...reclaimed.segments);
278
+ paragraph.endOffset = headerStart;
279
+ paragraph.endLine = Math.max(scanner.lineNumber - 2, paragraph.startLine);
280
+ }
281
+ const table = scanner.replaceBlock(container, "table");
282
+ table.stringContent = "";
283
+ table.segments.length = 0;
284
+ table.startOffset = headerStart;
285
+ table.startLine = Math.max(scanner.lineNumber - 1, 1);
286
+ const data = {
287
+ columns: headerRow.cells.length,
288
+ align: alignmentsOf(delimiterSource, delimiterRow),
289
+ rows: 0,
290
+ nonemptyCells: 0
291
+ };
292
+ table.data.tableData = data;
293
+ const headerEnd = offsetAt(headerSource, headerSource.text.length - 1, headerStart);
294
+ addRow(scanner, data, headerSource, headerRow, headerStart, headerEnd);
295
+ scanner.advanceOffset(scanner.currentLine.length - scanner.offset, false);
296
+ return 2;
297
+ }
298
+ };
299
+ /**
300
+ * The row start: any non-blank line under an open table is a row.
301
+ *
302
+ * Upstream's `try_opening_table_row`. It sits after every core block start, so
303
+ * a line that opens a blockquote, a list or a fence closes the table instead
304
+ * of becoming a row.
305
+ */
306
+ const tableRowStart = {
307
+ name: "tableRow",
308
+ trigger: (scanner, container) => {
309
+ if (scanner.indented || scanner.blank || container.type !== "table") return 0;
310
+ const data = container.data.tableData;
311
+ if (data === void 0) return 0;
312
+ if (data.columns * data.rows - data.nonemptyCells > MAX_AUTOCOMPLETED_CELLS) return 0;
313
+ const source = lineSource(scanner);
314
+ const row = rowFromSource(source);
315
+ if (row === void 0) return 0;
316
+ scanner.closeUnmatchedBlocks();
317
+ const rowStart = scanner.lineStart + scanner.nextNonspace;
318
+ addRow(scanner, data, source, row, rowStart, scanner.lineStart + scanner.currentLine.length);
319
+ scanner.advanceOffset(scanner.currentLine.length - scanner.offset, false);
320
+ return 2;
321
+ }
322
+ };
323
+ /** Table: contains rows, and continues for as long as lines scan as rows. */
324
+ const tableConstruct = {
325
+ type: "table",
326
+ acceptsLines: false,
327
+ canContain: (child) => child === "tableRow",
328
+ continue: (scanner) => {
329
+ if (scanner.indented) return 1;
330
+ return rowFromSource(lineSource(scanner)) === void 0 ? 1 : 0;
331
+ },
332
+ materialize: (block, children, context) => {
333
+ const rows = tableRowChildren(children);
334
+ if (rows.length === 0) return;
335
+ return Table.make({
336
+ align: block.data.tableData?.align ?? [],
337
+ children: rows,
338
+ position: context.position(block.startOffset, block.endOffset)
339
+ });
340
+ }
341
+ };
342
+ /** Table row: contains cells, and is built and closed on the line it opens. */
343
+ const tableRowConstruct = {
344
+ type: "tableRow",
345
+ acceptsLines: false,
346
+ canContain: (child) => child === "tableCell",
347
+ continue: () => 1,
348
+ materialize: (block, children, context) => TableRow.make({
349
+ children: tableCellChildren(children),
350
+ position: context.position(block.startOffset, block.endOffset)
351
+ })
352
+ };
353
+ /**
354
+ * Table cell: the block pass's one non-leaf inline host.
355
+ *
356
+ * Its content is the split, unescaped cell text with the source provenance of
357
+ * every surviving character, so it goes through the same `inlineSlice` seam a
358
+ * paragraph does — and inherits the trim, which is what makes the node's
359
+ * position span the cell's content rather than its padding.
360
+ */
361
+ const tableCellConstruct = {
362
+ type: "tableCell",
363
+ acceptsLines: false,
364
+ canContain: () => false,
365
+ continue: () => 1,
366
+ materialize: (block, _children, context) => {
367
+ const inline = context.inlineSlice(block);
368
+ const node = TableCell.make({
369
+ children: inline.children,
370
+ position: context.position(inline.startOffset, inline.endOffset)
371
+ });
372
+ context.registerInline(node, inline);
373
+ return node;
374
+ }
375
+ };
376
+
377
+ //#endregion
378
+ export { tableCellConstruct, tableConstruct, tableHeaderStart, tableRowConstruct, tableRowStart };
@@ -0,0 +1,36 @@
1
+ //#region src/internal/blocks/taskListItem.ts
2
+ /**
3
+ * `_scan_tasklist`, as the generated scanner accepts it.
4
+ *
5
+ * Two deliberate differences from the `ext_scanners.re` SOURCE rule, which is
6
+ * stale next to the C it generated: the bracket admits `X` as well as `x`, and
7
+ * `spacechar` is `[ \t\v\f]` — a line terminator does not end the marker, so
8
+ * `- [x]` alone is not a task item.
9
+ */
10
+ const reTaskListMarker = /^[ \t\v\f]*(?:[-+*]|\d+[^\n])[ \t\v\f]+\[[ xX]\][ \t\v\f]/;
11
+ /** The checkbox is three characters wide; upstream advances exactly that. */
12
+ const MARKER_LENGTH = 3;
13
+ /** Whether `line` carries a checked marker anywhere — upstream's `strstr` pair. */
14
+ const isChecked = (line) => line.includes("[x]") || line.includes("[X]");
15
+ /**
16
+ * The GFM task-list block start: a `[ ]`, `[x]` or `[X]` checkbox opening a
17
+ * list item's content.
18
+ *
19
+ * Registered in the `gfm` dialect only, immediately after the CommonMark
20
+ * starts, which is where cmark-gfm runs its extensions.
21
+ */
22
+ const taskListItemStart = {
23
+ name: "taskListItem",
24
+ trigger: (scanner, container) => {
25
+ if (container.type !== "listItem") return 0;
26
+ if (!reTaskListMarker.test(scanner.currentLine)) return 0;
27
+ container.data.checked = isChecked(scanner.currentLine);
28
+ scanner.advanceOffset(MARKER_LENGTH, false);
29
+ scanner.findNextNonspace();
30
+ scanner.advanceNextNonspace();
31
+ return 2;
32
+ }
33
+ };
34
+
35
+ //#endregion
36
+ export { taskListItemStart };
@@ -0,0 +1,35 @@
1
+ import { ThematicBreak } from "../../MarkdownNode.js";
2
+
3
+ //#region src/internal/blocks/thematicBreak.ts
4
+ const reThematicBreak = /^(?:\*[ \t]*){3,}$|^(?:_[ \t]*){3,}$|^(?:-[ \t]*){3,}$/;
5
+ const markerCharOf = (char) => char === "-" || char === "_" || char === "*" ? char : void 0;
6
+ /** Thematic break: one line, no children. */
7
+ const thematicBreakConstruct = {
8
+ type: "thematicBreak",
9
+ acceptsLines: false,
10
+ canContain: () => false,
11
+ continue: () => 1,
12
+ materialize: (block, _children, context) => {
13
+ const marker = block.data.markerChar;
14
+ return ThematicBreak.make({
15
+ position: context.position(block.startOffset, block.endOffset),
16
+ ...marker === void 0 ? {} : { markerChar: marker }
17
+ });
18
+ }
19
+ };
20
+ /** The thematic-break block start: three or more `-`, `_` or `*`. */
21
+ const thematicBreakStart = {
22
+ name: "thematicBreak",
23
+ trigger: (scanner) => {
24
+ if (scanner.indented || !reThematicBreak.test(scanner.currentLine.slice(scanner.nextNonspace))) return 0;
25
+ scanner.closeUnmatchedBlocks();
26
+ const container = scanner.addChild("thematicBreak", scanner.nextNonspace);
27
+ const marker = markerCharOf(scanner.currentLine.charAt(scanner.nextNonspace));
28
+ if (marker !== void 0) container.data.markerChar = marker;
29
+ scanner.advanceOffset(scanner.currentLine.length - scanner.offset, false);
30
+ return 2;
31
+ }
32
+ };
33
+
34
+ //#endregion
35
+ export { thematicBreakConstruct, thematicBreakStart };
@@ -0,0 +1,42 @@
1
+ //#region src/internal/carriers.ts
2
+ /**
3
+ * P1's error-code vocabulary. Widens as later phases add parse-error kinds;
4
+ * P1 registers exactly one, the hardening-guard trip.
5
+ */
6
+ const MARKDOWN_PARSE_ERROR_CODES = ["NestingDepthExceeded"];
7
+ /** The engine's carrier for a recoverable-turned-fatal parse condition. */
8
+ var RawMarkdownError = class extends Error {
9
+ diagnostic;
10
+ _tag = "RawMarkdownError";
11
+ constructor(diagnostic) {
12
+ super(diagnostic.message);
13
+ this.diagnostic = diagnostic;
14
+ }
15
+ };
16
+ /** Narrows `unknown` to {@link RawMarkdownError}; never a bare `instanceof` at a call site. */
17
+ const isRawMarkdownError = (u) => u instanceof RawMarkdownError;
18
+ /**
19
+ * Raw guard-trip signal. The engine throws it when a hardening cap
20
+ * (`MAX_NESTING_DEPTH`) is exceeded; it must never escape a public entry
21
+ * point as a defect — the facade catches it and materializes a typed
22
+ * `MarkdownParseError` carrying a `NestingDepthExceeded` diagnostic.
23
+ */
24
+ var GuardExceeded = class extends Error {
25
+ reason;
26
+ limit;
27
+ actual;
28
+ offset;
29
+ _tag = "GuardExceeded";
30
+ constructor(reason, limit, actual, offset) {
31
+ super(`${reason}: limit ${limit}, actual ${actual}`);
32
+ this.reason = reason;
33
+ this.limit = limit;
34
+ this.actual = actual;
35
+ this.offset = offset;
36
+ }
37
+ };
38
+ /** Narrows `unknown` to {@link GuardExceeded}; never a bare `instanceof` at a call site. */
39
+ const isGuardExceeded = (u) => u instanceof GuardExceeded;
40
+
41
+ //#endregion
42
+ export { GuardExceeded, MARKDOWN_PARSE_ERROR_CODES, RawMarkdownError, isGuardExceeded, isRawMarkdownError };
@@ -0,0 +1,26 @@
1
+ import { ENTITY_MAP } from "./entityMap.js";
2
+
3
+ //#region src/internal/entities.ts
4
+ /** The largest code point Unicode defines. */
5
+ const MAX_CODE_POINT = 1114111;
6
+ /**
7
+ * Decode one character reference, brackets included (`&#35;`, `&amp;`).
8
+ *
9
+ * Returns `undefined` when the reference is not one this engine can decode,
10
+ * which the caller must render as the literal source text — never as an
11
+ * empty string.
12
+ */
13
+ const decodeEntity = (entity) => {
14
+ if (!entity.startsWith("&") || !entity.endsWith(";")) return;
15
+ const body = entity.slice(1, -1);
16
+ if (!body.startsWith("#")) return ENTITY_MAP.get(body);
17
+ const hex = body.charAt(1) === "x" || body.charAt(1) === "X";
18
+ const digits = hex ? body.slice(2) : body.slice(1);
19
+ if (digits.length === 0 || !(hex ? /^[0-9a-f]+$/i : /^[0-9]+$/).test(digits)) return;
20
+ const code = Number.parseInt(digits, hex ? 16 : 10);
21
+ if (code === 0 || code > MAX_CODE_POINT || code >= 55296 && code <= 57343) return "�";
22
+ return String.fromCodePoint(code);
23
+ };
24
+
25
+ //#endregion
26
+ export { decodeEntity };