@fuzdev/mdz 0.1.0 → 0.3.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 (77) hide show
  1. package/README.md +4 -0
  2. package/dist/Mdz.svelte +15 -7
  3. package/dist/Mdz.svelte.d.ts +9 -2
  4. package/dist/Mdz.svelte.d.ts.map +1 -1
  5. package/dist/MdzNodeView.svelte +130 -95
  6. package/dist/MdzNodeView.svelte.d.ts +6 -2
  7. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  8. package/dist/MdzStream.svelte +3 -3
  9. package/dist/MdzStream.svelte.d.ts.map +1 -1
  10. package/dist/MdzStreamNodeView.svelte +129 -95
  11. package/dist/MdzStreamNodeView.svelte.d.ts +6 -2
  12. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
  13. package/dist/context_helpers.d.ts +1 -1
  14. package/dist/mdz.d.ts +59 -2
  15. package/dist/mdz.d.ts.map +1 -1
  16. package/dist/mdz.js +37 -1
  17. package/dist/mdz_debug_work.d.ts +38 -0
  18. package/dist/mdz_debug_work.d.ts.map +1 -0
  19. package/dist/mdz_debug_work.js +37 -0
  20. package/dist/mdz_helpers.d.ts +96 -1
  21. package/dist/mdz_helpers.d.ts.map +1 -1
  22. package/dist/mdz_helpers.js +192 -0
  23. package/dist/mdz_lexer.d.ts +47 -1
  24. package/dist/mdz_lexer.d.ts.map +1 -1
  25. package/dist/mdz_lexer.js +364 -15
  26. package/dist/mdz_opcodes.d.ts +6 -1
  27. package/dist/mdz_opcodes.d.ts.map +1 -1
  28. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
  29. package/dist/mdz_opcodes_to_nodes.js +26 -0
  30. package/dist/mdz_stream_parser.d.ts.map +1 -1
  31. package/dist/mdz_stream_parser.js +20 -1
  32. package/dist/mdz_stream_parser_inline.d.ts.map +1 -1
  33. package/dist/mdz_stream_parser_inline.js +19 -17
  34. package/dist/mdz_stream_parser_link.d.ts.map +1 -1
  35. package/dist/mdz_stream_parser_link.js +88 -56
  36. package/dist/mdz_stream_parser_list.d.ts.map +1 -1
  37. package/dist/mdz_stream_parser_list.js +32 -3
  38. package/dist/mdz_stream_parser_state.d.ts +41 -0
  39. package/dist/mdz_stream_parser_state.d.ts.map +1 -1
  40. package/dist/mdz_stream_parser_state.js +53 -3
  41. package/dist/mdz_stream_parser_table.d.ts +73 -0
  42. package/dist/mdz_stream_parser_table.d.ts.map +1 -0
  43. package/dist/mdz_stream_parser_table.js +362 -0
  44. package/dist/mdz_stream_parser_text.d.ts.map +1 -1
  45. package/dist/mdz_stream_parser_text.js +35 -24
  46. package/dist/mdz_stream_state.svelte.d.ts +5 -0
  47. package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
  48. package/dist/mdz_stream_state.svelte.js +8 -0
  49. package/dist/mdz_to_svelte.d.ts.map +1 -1
  50. package/dist/mdz_to_svelte.js +51 -20
  51. package/dist/mdz_token_parser.d.ts +16 -0
  52. package/dist/mdz_token_parser.d.ts.map +1 -1
  53. package/dist/mdz_token_parser.js +112 -14
  54. package/dist/svelte_preprocess_mdz.js +33 -39
  55. package/package.json +11 -8
  56. package/src/lib/context_helpers.ts +1 -1
  57. package/src/lib/mdz.ts +79 -3
  58. package/src/lib/mdz_contexts.ts +1 -1
  59. package/src/lib/mdz_debug_work.ts +38 -0
  60. package/src/lib/mdz_helpers.ts +222 -1
  61. package/src/lib/mdz_lexer.ts +420 -21
  62. package/src/lib/mdz_opcodes.ts +10 -1
  63. package/src/lib/mdz_opcodes_to_nodes.ts +37 -3
  64. package/src/lib/mdz_stream_parser.ts +26 -9
  65. package/src/lib/mdz_stream_parser_block.ts +2 -2
  66. package/src/lib/mdz_stream_parser_inline.ts +25 -21
  67. package/src/lib/mdz_stream_parser_link.ts +92 -59
  68. package/src/lib/mdz_stream_parser_list.ts +34 -6
  69. package/src/lib/mdz_stream_parser_state.ts +87 -5
  70. package/src/lib/mdz_stream_parser_table.ts +420 -0
  71. package/src/lib/mdz_stream_parser_text.ts +40 -39
  72. package/src/lib/mdz_stream_parser_url.ts +2 -2
  73. package/src/lib/mdz_stream_state.svelte.ts +8 -1
  74. package/src/lib/mdz_to_svelte.ts +49 -23
  75. package/src/lib/mdz_token_parser.ts +135 -21
  76. package/src/lib/svelte_preprocess_mdz.ts +37 -38
  77. package/src/lib/tsdoc_mdz.ts +1 -1
@@ -7,7 +7,7 @@
7
7
  * @module
8
8
  */
9
9
 
10
- import type {MdzNode, MdzNodeText, MdzNodeComponent, MdzNodeElement} from './mdz.js';
10
+ import type {MdzNode, MdzNodeText, MdzNodeComponent, MdzNodeElement, MdzTableAlign} from './mdz.ts';
11
11
 
12
12
  // Character codes for performance
13
13
  /** @nodocs */
@@ -47,6 +47,10 @@ export const RIGHT_PAREN = 41; // )
47
47
  /** @nodocs */
48
48
  export const COLON = 58; // :
49
49
  /** @nodocs */
50
+ export const PIPE = 124; // |
51
+ /** @nodocs */
52
+ export const BACKSLASH = 92; // \
53
+ /** @nodocs */
50
54
  export const PERIOD = 46; // .
51
55
  /** @nodocs */
52
56
  export const COMMA = 44; // ,
@@ -95,6 +99,23 @@ export const MIN_CODEBLOCK_BACKTICKS = 3; // Code blocks require minimum 3 backt
95
99
  export const MAX_LIST_NUMBER_DIGITS = 9; // Ordered list markers cap at 9 digits (CommonMark's cap)
96
100
  /** @nodocs */
97
101
  export const MAX_HEADING_LEVEL = 6; // Headings support levels 1-6
102
+ /**
103
+ * Cap on inline link/tag nesting depth. Reached at this many open
104
+ * `Link`/`Element`/`Component` containers, a further `[` or `<tag>` opener
105
+ * renders as literal text in both parsers rather than opening — a strict
106
+ * untrusted-content dialect's bound on pathological nesting (CommonMark
107
+ * permits such a limit). It bounds both the sync lexer's recursion depth (no
108
+ * stack overflow) and, with the failure memo, its revert re-scan work (linear
109
+ * instead of quadratic) on adversarial nested `[`/`<`. Deliberately far above
110
+ * any real content (inline nesting rarely exceeds a handful); only pathological
111
+ * input reaches it. Mirrored in the streaming parser so the two agree on where
112
+ * the cap bites. Delimiters (`**`/`_`/`~~`/`` ` ``) are uncounted: first-closer-wins
113
+ * keeps them from nesting deeply on their own, and mixed delimiter/link chains
114
+ * only deep-nest through the links, which this bounds.
115
+ *
116
+ * @nodocs
117
+ */
118
+ export const MAX_INLINE_NESTING_DEPTH = 100;
98
119
  /** @nodocs */
99
120
  export const HTTPS_PREFIX_LENGTH = 8; // Length of "https://"
100
121
  /** @nodocs */
@@ -287,6 +308,126 @@ export const mdz_remap_segments = (
287
308
  return seg.source_start + (p - seg.inner_start);
288
309
  };
289
310
 
311
+ // -- Table helpers --
312
+
313
+ /** Trim line-whitespace from both ends of a `[start, end)` span. */
314
+ const trim_table_cell = (
315
+ text: string,
316
+ start: number,
317
+ end: number,
318
+ ): {start: number; end: number} => {
319
+ let s = start;
320
+ let e = end;
321
+ while (s < e && is_line_whitespace(text.charCodeAt(s))) s++;
322
+ while (e > s && is_line_whitespace(text.charCodeAt(e - 1))) e--;
323
+ return {start: s, end: e};
324
+ };
325
+
326
+ /**
327
+ * Split a table row line `[line_start, line_end)` into its cell-content spans.
328
+ *
329
+ * A valid row starts with `|` and ends — after optional trailing whitespace —
330
+ * with `|`, yielding at least one cell. Returns each cell's trimmed
331
+ * `[start, end)` source span, or `null` when the line is not a valid pipe row
332
+ * (no leading `|`, no trailing `|`, or zero cells).
333
+ *
334
+ * Pipe-finding honors two protections so the cell content can hold a literal
335
+ * `|`: an inline-code span (a `` ` `` to its matching `` ` `` on the same line —
336
+ * an unclosed `` ` `` is literal and protects nothing) shields the pipes inside
337
+ * it, and a `\|` escape is consumed as a literal pipe. The escape is
338
+ * table-cell-scoped — the caller's inline tokenizer performs the matching
339
+ * `\|` → `|` substitution; this scanner only declines to split there.
340
+ *
341
+ * @nodocs
342
+ */
343
+ export const mdz_split_table_row = (
344
+ text: string,
345
+ line_start: number,
346
+ line_end: number,
347
+ ): Array<{start: number; end: number}> | null => {
348
+ if (line_start >= line_end || text.charCodeAt(line_start) !== PIPE) return null;
349
+ const cells: Array<{start: number; end: number}> = [];
350
+ let i = line_start + 1; // past the leading pipe
351
+ let cell_start = i;
352
+ while (i < line_end) {
353
+ const char_code = text.charCodeAt(i);
354
+ if (char_code === BACKTICK) {
355
+ // inline-code span: skip to the matching backtick; unclosed → literal
356
+ let close = -1;
357
+ for (let k = i + 1; k < line_end; k++) {
358
+ if (text.charCodeAt(k) === BACKTICK) {
359
+ close = k;
360
+ break;
361
+ }
362
+ }
363
+ if (close === -1) {
364
+ i++;
365
+ } else {
366
+ i = close + 1;
367
+ }
368
+ continue;
369
+ }
370
+ if (char_code === BACKSLASH) {
371
+ // `\|` is a literal pipe, never a delimiter
372
+ i += i + 1 < line_end && text.charCodeAt(i + 1) === PIPE ? 2 : 1;
373
+ continue;
374
+ }
375
+ if (char_code === PIPE) {
376
+ cells.push(trim_table_cell(text, cell_start, i));
377
+ i++;
378
+ cell_start = i;
379
+ continue;
380
+ }
381
+ i++;
382
+ }
383
+ // the trailing pipe is required: only whitespace may follow the last one
384
+ for (let k = cell_start; k < line_end; k++) {
385
+ if (!is_line_whitespace(text.charCodeAt(k))) return null;
386
+ }
387
+ if (cells.length === 0) return null;
388
+ return cells;
389
+ };
390
+
391
+ /** Classify a trimmed delimiter cell `[start, end)`; `false` when it is not a valid delimiter cell. */
392
+ const table_delimiter_align = (text: string, start: number, end: number): MdzTableAlign | false => {
393
+ let i = start;
394
+ let j = end;
395
+ if (i >= j) return false;
396
+ const left = text.charCodeAt(i) === COLON;
397
+ if (left) i++;
398
+ const right = j > i && text.charCodeAt(j - 1) === COLON;
399
+ if (right) j--;
400
+ if (i >= j) return false; // needs at least one dash
401
+ for (let k = i; k < j; k++) {
402
+ if (text.charCodeAt(k) !== HYPHEN) return false;
403
+ }
404
+ return left && right ? 'center' : left ? 'left' : right ? 'right' : null;
405
+ };
406
+
407
+ /**
408
+ * Parse a table delimiter row (`| --- | :-: | ---: |`) into its per-column
409
+ * alignment, or `null` when the line is not a valid delimiter row. Each cell
410
+ * must be an optional leading `:`, one or more `-`, and an optional trailing
411
+ * `:`. Splits with `mdz_split_table_row` so the column count matches the header.
412
+ *
413
+ * @nodocs
414
+ */
415
+ export const mdz_parse_table_delimiter = (
416
+ text: string,
417
+ line_start: number,
418
+ line_end: number,
419
+ ): Array<MdzTableAlign> | null => {
420
+ const cells = mdz_split_table_row(text, line_start, line_end);
421
+ if (cells === null) return null;
422
+ const align: Array<MdzTableAlign> = [];
423
+ for (const cell of cells) {
424
+ const a = table_delimiter_align(text, cell.start, cell.end);
425
+ if (a === false) return null;
426
+ align.push(a);
427
+ }
428
+ return align;
429
+ };
430
+
290
431
  /**
291
432
  * Lowercase an ASCII letter code; passes other chars through.
292
433
  * Mirrors the `[A-Z] → [a-z]` shift without touching non-ASCII.
@@ -423,6 +564,40 @@ const PATH_CHAR_TABLE: Uint8Array = (() => {
423
564
  export const is_valid_path_char = (char_code: number): boolean =>
424
565
  char_code < 128 && PATH_CHAR_TABLE[char_code] === 1;
425
566
 
567
+ // Character classes for the plain-text run scanners (the hottest loops in
568
+ // both parsers) — one table load per character instead of a comparison chain.
569
+ // A zero class is the common case: the char can't end or interrupt a text run.
570
+
571
+ /** Always ends a text run: `` ` `` `*` `_` `~` `[` `]` `<`. @nodocs */
572
+ export const TEXT_CLASS_STOP = 1;
573
+ /** `\n` — run handling is context-dependent (soft break, paragraph break, block lookahead). @nodocs */
574
+ export const TEXT_CLASS_NEWLINE = 2;
575
+ /** `h`/`H` — potential URL scheme start, needs the prefix probe. @nodocs */
576
+ export const TEXT_CLASS_URL = 4;
577
+ /** `/` and `.` — potential path start, needs the boundary probe. @nodocs */
578
+ export const TEXT_CLASS_PATH = 8;
579
+ /** `\` — a literal-pipe escape candidate inside table cells only. @nodocs */
580
+ export const TEXT_CLASS_BACKSLASH = 16;
581
+
582
+ /** @nodocs */
583
+ export const TEXT_SCAN_CLASS: Uint8Array = (() => {
584
+ const t = new Uint8Array(128);
585
+ t[BACKTICK] = TEXT_CLASS_STOP;
586
+ t[ASTERISK] = TEXT_CLASS_STOP;
587
+ t[UNDERSCORE] = TEXT_CLASS_STOP;
588
+ t[TILDE] = TEXT_CLASS_STOP;
589
+ t[LEFT_BRACKET] = TEXT_CLASS_STOP;
590
+ t[RIGHT_BRACKET] = TEXT_CLASS_STOP;
591
+ t[LEFT_ANGLE] = TEXT_CLASS_STOP;
592
+ t[NEWLINE] = TEXT_CLASS_NEWLINE;
593
+ t[H_LOWER] = TEXT_CLASS_URL;
594
+ t[H_UPPER] = TEXT_CLASS_URL;
595
+ t[SLASH] = TEXT_CLASS_PATH;
596
+ t[PERIOD] = TEXT_CLASS_PATH;
597
+ t[BACKSLASH] = TEXT_CLASS_BACKSLASH;
598
+ return t;
599
+ })();
600
+
426
601
  /**
427
602
  * Trim trailing punctuation from URL/path per RFC 3986 and GFM rules.
428
603
  * - Trims simple trailing: .,;:!?]
@@ -636,6 +811,52 @@ export const mdz_resolve_relative_path = (reference: string, base: string): stri
636
811
  return segments.join('/');
637
812
  };
638
813
 
814
+ /**
815
+ * How a `Link` node's `reference` should render, shared by all three renderers
816
+ * (`MdzNodeView`, `MdzStreamNodeView`, `mdz_to_svelte`) so the safety gate and
817
+ * the resolve-vs-raw classification can't drift between them.
818
+ * @nodocs
819
+ */
820
+ export type MdzLinkRender =
821
+ /** Unsafe protocol — render children only, no `<a>`. */
822
+ | {kind: 'unsafe'}
823
+ /** Route/relative path — wrap in `resolve()` (needs `$app/paths`). */
824
+ | {kind: 'resolve'; href: string}
825
+ /** Internal fragment/query/relative/bare ref — raw `href`, no `resolve()`. */
826
+ | {kind: 'internal'; href: string}
827
+ /** External link — raw `href` plus `target="_blank" rel="noopener"`. */
828
+ | {kind: 'external'; href: string};
829
+
830
+ /**
831
+ * Classify a `Link` reference into how it should render. Pure — no rendering,
832
+ * no context; `base` is the resolved base path (or `undefined`).
833
+ * @nodocs
834
+ */
835
+ export const mdz_classify_link = (
836
+ reference: string,
837
+ link_type: 'internal' | 'external' | undefined,
838
+ base: string | undefined,
839
+ ): MdzLinkRender => {
840
+ if (!mdz_is_safe_reference(reference)) return {kind: 'unsafe'};
841
+ if (link_type === 'internal') {
842
+ if (reference.startsWith('.') && base) {
843
+ return {kind: 'resolve', href: mdz_resolve_relative_path(reference, base)};
844
+ }
845
+ // fragment/query/relative/bare — `resolve()` only accepts absolute paths or
846
+ // route ids and throws on anything else
847
+ if (
848
+ reference.startsWith('#') ||
849
+ reference.startsWith('?') ||
850
+ reference.startsWith('.') ||
851
+ !reference.startsWith('/')
852
+ ) {
853
+ return {kind: 'internal', href: reference};
854
+ }
855
+ return {kind: 'resolve', href: reference};
856
+ }
857
+ return {kind: 'external', href: reference};
858
+ };
859
+
639
860
  /**
640
861
  * Push a node into a children array, coalescing with the previous Text node.
641
862
  * Mutates `prev.content` and `prev.end` when both are Text, avoiding array growth