@fuzdev/mdz 0.8.1 → 0.10.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 (64) hide show
  1. package/dist/Mdz.svelte +37 -1
  2. package/dist/Mdz.svelte.d.ts +35 -0
  3. package/dist/Mdz.svelte.d.ts.map +1 -1
  4. package/dist/MdzNodeView.svelte +143 -27
  5. package/dist/MdzNodeView.svelte.d.ts +13 -1
  6. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  7. package/dist/MdzPrecompiled.svelte.d.ts.map +1 -1
  8. package/dist/MdzRoot.svelte.d.ts.map +1 -1
  9. package/dist/MdzStream.svelte.d.ts.map +1 -1
  10. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
  11. package/dist/context_helpers.d.ts.map +1 -1
  12. package/dist/mdz.d.ts +4 -1
  13. package/dist/mdz.d.ts.map +1 -1
  14. package/dist/mdz.js +4 -1
  15. package/dist/mdz_contexts.d.ts +14 -2
  16. package/dist/mdz_contexts.d.ts.map +1 -1
  17. package/dist/mdz_debug_work.d.ts.map +1 -1
  18. package/dist/mdz_helpers.d.ts +57 -7
  19. package/dist/mdz_helpers.d.ts.map +1 -1
  20. package/dist/mdz_helpers.js +113 -50
  21. package/dist/mdz_lexer.d.ts.map +1 -1
  22. package/dist/mdz_lexer.js +21 -17
  23. package/dist/mdz_opcodes.d.ts.map +1 -1
  24. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
  25. package/dist/mdz_stream_parser.d.ts +16 -3
  26. package/dist/mdz_stream_parser.d.ts.map +1 -1
  27. package/dist/mdz_stream_parser.js +16 -3
  28. package/dist/mdz_stream_parser_block.d.ts +2 -2
  29. package/dist/mdz_stream_parser_block.d.ts.map +1 -1
  30. package/dist/mdz_stream_parser_block.js +2 -2
  31. package/dist/mdz_stream_parser_inline.d.ts.map +1 -1
  32. package/dist/mdz_stream_parser_inline.js +4 -4
  33. package/dist/mdz_stream_parser_link.d.ts.map +1 -1
  34. package/dist/mdz_stream_parser_list.d.ts.map +1 -1
  35. package/dist/mdz_stream_parser_state.d.ts +32 -12
  36. package/dist/mdz_stream_parser_state.d.ts.map +1 -1
  37. package/dist/mdz_stream_parser_state.js +21 -14
  38. package/dist/mdz_stream_parser_table.d.ts.map +1 -1
  39. package/dist/mdz_stream_parser_text.d.ts.map +1 -1
  40. package/dist/mdz_stream_parser_text.js +7 -4
  41. package/dist/mdz_stream_parser_url.d.ts +3 -2
  42. package/dist/mdz_stream_parser_url.d.ts.map +1 -1
  43. package/dist/mdz_stream_parser_url.js +114 -42
  44. package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
  45. package/dist/mdz_to_svelte.d.ts.map +1 -1
  46. package/dist/mdz_token_parser.d.ts.map +1 -1
  47. package/dist/svelte_preprocess_mdz.d.ts.map +1 -1
  48. package/dist/svelte_preprocess_mdz.js +8 -0
  49. package/dist/tsdoc_mdz.d.ts +8 -4
  50. package/dist/tsdoc_mdz.d.ts.map +1 -1
  51. package/dist/tsdoc_mdz.js +14 -4
  52. package/package.json +7 -7
  53. package/src/lib/mdz.ts +4 -1
  54. package/src/lib/mdz_contexts.ts +20 -2
  55. package/src/lib/mdz_helpers.ts +117 -49
  56. package/src/lib/mdz_lexer.ts +31 -14
  57. package/src/lib/mdz_stream_parser.ts +16 -3
  58. package/src/lib/mdz_stream_parser_block.ts +2 -2
  59. package/src/lib/mdz_stream_parser_inline.ts +4 -4
  60. package/src/lib/mdz_stream_parser_state.ts +34 -13
  61. package/src/lib/mdz_stream_parser_text.ts +13 -6
  62. package/src/lib/mdz_stream_parser_url.ts +130 -49
  63. package/src/lib/svelte_preprocess_mdz.ts +8 -0
  64. package/src/lib/tsdoc_mdz.ts +15 -4
@@ -18,8 +18,14 @@ import {
18
18
  SPACE,
19
19
  TAB,
20
20
  PERIOD,
21
+ LEFT_PAREN,
22
+ RIGHT_PAREN,
21
23
  SLASH,
22
24
  ascii_to_lower,
25
+ is_bare_path_prefix,
26
+ is_italic_boundary_blocker,
27
+ scan_uri_chars_end,
28
+ is_path_boundary,
23
29
  is_valid_path_char,
24
30
  is_word_char,
25
31
  match_url_prefix_case_insensitive,
@@ -33,7 +39,9 @@ import {
33
39
  emit,
34
40
  ensure_paragraph,
35
41
  flush_text,
36
- offset
42
+ is_run_boundary,
43
+ offset,
44
+ revert_failed_close
37
45
  } from './mdz_stream_parser_state.ts';
38
46
 
39
47
  const HTTPS_PREFIX = 'https://';
@@ -62,15 +70,8 @@ export const try_auto_url_forced = (state: MdzStreamParserState): TryResult => {
62
70
 
63
71
  /** @nodocs */
64
72
  export const try_auto_path_absolute = (state: MdzStreamParserState, forced = false): TryResult => {
65
- // must be at word boundary (preceded by space/newline/start)
66
- if (
67
- state.prev_char !== -1 &&
68
- state.prev_char !== SPACE &&
69
- state.prev_char !== NEWLINE &&
70
- state.prev_char !== TAB
71
- ) {
72
- return 'not_match';
73
- }
73
+ // must be at a path boundary (start, whitespace, or `(`)
74
+ if (!is_path_boundary(state.prev_char, state.before_prev_char)) return 'not_match';
74
75
 
75
76
  // reject // and /space
76
77
  if (state.pos + 1 >= state.buffer.length) return forced ? 'not_match' : 'need_more';
@@ -86,14 +87,8 @@ export const try_auto_path_absolute = (state: MdzStreamParserState, forced = fal
86
87
 
87
88
  /** @nodocs */
88
89
  export const try_auto_path_relative = (state: MdzStreamParserState, forced = false): TryResult => {
89
- if (
90
- state.prev_char !== -1 &&
91
- state.prev_char !== SPACE &&
92
- state.prev_char !== NEWLINE &&
93
- state.prev_char !== TAB
94
- ) {
95
- return 'not_match';
96
- }
90
+ // must be at a path boundary (start, whitespace, or `(`)
91
+ if (!is_path_boundary(state.prev_char, state.before_prev_char)) return 'not_match';
97
92
 
98
93
  // check for ./ or ../
99
94
  const remaining = state.buffer.length - state.pos;
@@ -147,7 +142,9 @@ export const start_pending_url = (
147
142
  link_type,
148
143
  confirmed: true,
149
144
  viable_https: false,
150
- viable_http: false
145
+ viable_http: false,
146
+ // a path opened by `(` ends at the `)` matching it
147
+ paren_depth: state.prev_char === LEFT_PAREN ? 0 : -1
151
148
  };
152
149
  // don't advance pos — URL chars start at current position, handled by process_url_content
153
150
  };
@@ -169,7 +166,8 @@ export const start_speculative_url = (state: MdzStreamParserState): void => {
169
166
  link_type: 'external',
170
167
  confirmed: false,
171
168
  viable_https: true,
172
- viable_http: true
169
+ viable_http: true,
170
+ paren_depth: -1
173
171
  };
174
172
  };
175
173
 
@@ -227,8 +225,9 @@ export const process_url_prefix = (state: MdzStreamParserState): void => {
227
225
 
228
226
  /**
229
227
  * Process URL/path chars in text-first mode. Scans path chars,
230
- * accumulates as visible text. Terminates on space/newline/non-path-char
231
- * and emits a `wrap` opcode.
228
+ * accumulates as visible text. Terminates on space/newline/non-path-char —
229
+ * or, for a path opened by `(`, on the `)` matching that opener — and emits
230
+ * a `wrap` opcode.
232
231
  *
233
232
  * @nodocs
234
233
  */
@@ -239,37 +238,108 @@ export const process_url_content = (state: MdzStreamParserState, forced: boolean
239
238
  return;
240
239
  }
241
240
 
242
- const char_code = state.buffer.charCodeAt(state.pos);
243
-
244
- // URL terminator
245
- if (char_code === SPACE || char_code === NEWLINE || !is_valid_path_char(char_code)) {
246
- flush_text(state);
247
- complete_pending_url(state);
248
- // don't consume terminator — outer loop handles it
249
- return;
250
- }
251
-
252
- // scan URL chars
241
+ const pu = state.pending_url!;
253
242
  const start = state.pos;
254
- state.pos++;
255
243
  while (state.pos < state.buffer.length) {
256
244
  const c = state.buffer.charCodeAt(state.pos);
257
- if (c === SPACE || c === NEWLINE || !is_valid_path_char(c)) break;
245
+ if (!is_valid_path_char(c)) break;
246
+ if (pu.paren_depth !== -1) {
247
+ if (c === LEFT_PAREN) pu.paren_depth++;
248
+ else if (c === RIGHT_PAREN) {
249
+ if (pu.paren_depth === 0) break;
250
+ pu.paren_depth--;
251
+ }
252
+ }
258
253
  state.pos++;
259
254
  }
260
- const text = state.buffer.slice(start, state.pos);
261
- accumulate_text(state, text, offset(state, start));
262
- state.pending_url!.url_text += text;
263
- state.prev_char = state.buffer.charCodeAt(state.pos - 1);
264
- state.column += state.pos - start;
265
-
266
- // at EOF in forced mode, finalize the URL
267
- if (state.pos >= state.buffer.length && forced) {
255
+
256
+ if (state.pos > start) {
257
+ const text = state.buffer.slice(start, state.pos);
258
+ accumulate_text(state, text, offset(state, start));
259
+ pu.url_text += text;
260
+ state.prev_char = state.buffer.charCodeAt(state.pos - 1);
261
+ state.column += state.pos - start;
262
+ }
263
+
264
+ // a terminator in the buffer ends the URL (not consumed — the outer loop
265
+ // handles it); at EOF in forced mode the buffer end does
266
+ if (state.pos < state.buffer.length || forced) {
268
267
  flush_text(state);
269
268
  complete_pending_url(state);
270
269
  }
271
270
  };
272
271
 
272
+ // the char at the parse position, or `-1` at the end of the buffer
273
+ const char_after_pos = (state: MdzStreamParserState): number =>
274
+ state.pos < state.buffer.length ? state.buffer.charCodeAt(state.pos) : -1;
275
+
276
+ /**
277
+ * Resolve the open spans whose first closer candidate an auto-link swallowed,
278
+ * the way the one-shot parse does. The lexer pairs each `**`/`~~`/`_` opener
279
+ * with the first occurrence of its delimiter after it
280
+ * (`#tokenize_double_delimiter`, `#tokenize_italic`). Streaming resolves each
281
+ * open span's earlier candidates as they arrive, so a delimiter inside the
282
+ * link's reference is that first occurrence, and the lexer's outcome depends
283
+ * on the span:
284
+ *
285
+ * - `**` and `~~` pair with the occurrence unconditionally; the span's children
286
+ * lex bounded by it, the link overruns it, and the opener stays literal —
287
+ * as does every delimiter, tag, and code opener lexed inside the span (each
288
+ * bounded by the swallowed closer). Markdown links aren't bounded by an
289
+ * enclosing span, so Link frames survive.
290
+ * - `_` first checks the candidate's closing boundary (`is_italic_boundary_blocker`
291
+ * on the char after it). A blocked candidate rejects only the opener — the
292
+ * span's content then lexes unbounded, so inner spans survive — while a
293
+ * valid one pairs and overruns like the doubled delimiters.
294
+ *
295
+ * So: the lowest span in the current run that pairs inside the reference is
296
+ * reverted along with every non-Link frame above it, and an italic below it
297
+ * with a blocked candidate is reverted alone (mid-stack, frames above it kept).
298
+ * Called after the link's `wrap`/`close` is emitted, so a pending wrap never
299
+ * outlives an enclosing revert.
300
+ *
301
+ * @param scanned - the scanned URL/path text, starting with `reference` and
302
+ * including any trimmed tail
303
+ * @param after_scanned - the char code after `scanned`, or `-1` at end of input
304
+ */
305
+ const revert_spans_closed_inside = (
306
+ state: MdzStreamParserState,
307
+ reference: string,
308
+ scanned: string,
309
+ after_scanned: number
310
+ ): void => {
311
+ let run_start = 0;
312
+ for (let i = state.stack.length - 1; i >= 0; i--) {
313
+ if (is_run_boundary(state.stack[i]!.node_type)) {
314
+ run_start = i + 1;
315
+ break;
316
+ }
317
+ }
318
+ let paired = -1; // lowest span pairing inside the reference
319
+ let rejected_italic = -1; // an italic whose candidate's boundary is blocked
320
+ for (let i = run_start; i < state.stack.length && paired === -1; i++) {
321
+ const entry = state.stack[i]!;
322
+ if (entry.node_type === 'Bold' || entry.node_type === 'Strikethrough') {
323
+ if (reference.includes(entry.delimiter)) paired = i;
324
+ } else if (entry.node_type === 'Italic') {
325
+ const k = reference.indexOf('_');
326
+ if (k === -1) continue;
327
+ const after = k + 1 < scanned.length ? scanned.charCodeAt(k + 1) : after_scanned;
328
+ if (after !== -1 && is_italic_boundary_blocker(after)) rejected_italic = i;
329
+ else paired = i;
330
+ }
331
+ }
332
+ if (paired !== -1) {
333
+ for (let i = state.stack.length - 1; i >= paired; i--) {
334
+ if (state.stack[i]!.node_type !== 'Link') revert_failed_close(state, i);
335
+ }
336
+ }
337
+ // below `paired` (or with nothing paired) — indices below it are unchanged
338
+ if (rejected_italic !== -1 && (paired === -1 || rejected_italic < paired)) {
339
+ revert_failed_close(state, rejected_italic);
340
+ }
341
+ };
342
+
273
343
  /**
274
344
  * Complete a pending URL: compute reference with trimmed punctuation,
275
345
  * emit a `wrap` opcode to retroactively wrap the text node in a Link.
@@ -287,6 +357,8 @@ export const complete_pending_url = (state: MdzStreamParserState): void => {
287
357
 
288
358
  const reference = mdz_trim_url_punctuation(pu.url_text);
289
359
  if (reference.length === 0) return; // empty after trimming — text stays as plain text
360
+ // a bare path prefix (`./`, `/`) left after trimming isn't a path — text stays
361
+ if (pu.link_type === 'internal' && is_bare_path_prefix(reference)) return;
290
362
 
291
363
  // for external URLs, must have content after protocol prefix
292
364
  if (pu.link_type === 'external') {
@@ -316,6 +388,7 @@ export const complete_pending_url = (state: MdzStreamParserState): void => {
316
388
  trim_id
317
389
  });
318
390
  state.active_text_id = trim_id;
391
+ revert_spans_closed_inside(state, reference, pu.url_text, char_after_pos(state));
319
392
  } else {
320
393
  emit(state, {
321
394
  type: 'wrap',
@@ -328,6 +401,7 @@ export const complete_pending_url = (state: MdzStreamParserState): void => {
328
401
  end: url_end
329
402
  });
330
403
  state.active_text_id = null;
404
+ revert_spans_closed_inside(state, reference, pu.url_text, char_after_pos(state));
331
405
  }
332
406
  };
333
407
 
@@ -341,16 +415,17 @@ export const scan_auto_link_forced = (
341
415
  state: MdzStreamParserState,
342
416
  link_type: 'external' | 'internal'
343
417
  ): TryResult => {
344
- let i = state.pos;
345
- while (i < state.buffer.length) {
346
- const c = state.buffer.charCodeAt(i);
347
- if (c === SPACE || c === NEWLINE || !is_valid_path_char(c)) break;
348
- i++;
349
- }
418
+ // a path opened by `(` ends at the `)` matching it
419
+ const i = scan_uri_chars_end(
420
+ state.buffer,
421
+ state.pos,
422
+ link_type === 'internal' && state.prev_char === LEFT_PAREN
423
+ );
350
424
 
351
425
  let reference = state.buffer.slice(state.pos, i);
352
426
  reference = mdz_trim_url_punctuation(reference);
353
427
  if (reference.length === 0) return 'not_match';
428
+ if (link_type === 'internal' && is_bare_path_prefix(reference)) return 'not_match';
354
429
 
355
430
  // for external URLs, must have content after protocol prefix
356
431
  if (link_type === 'external') {
@@ -382,5 +457,11 @@ export const scan_auto_link_forced = (
382
457
  state.column += end_pos - state.pos;
383
458
  state.prev_char = reference.charCodeAt(reference.length - 1);
384
459
  state.pos = end_pos;
460
+ revert_spans_closed_inside(
461
+ state,
462
+ reference,
463
+ state.buffer.slice(link_start - state.base_offset, i),
464
+ i < state.buffer.length ? state.buffer.charCodeAt(i) : -1
465
+ );
385
466
  return 'consumed';
386
467
  };
@@ -397,9 +397,17 @@ const find_mdz_usages = (
397
397
  const inline = inline_attr ? extract_static_boolean(inline_attr.value) : false;
398
398
  if (inline === null) return; // dynamic inline — fall back to runtime
399
399
 
400
+ // precompiled markup carries no source offsets, so content that asks for
401
+ // them renders at runtime; a static `false` asks for nothing and is dropped
402
+ const source_offsets_attr = find_attribute(node, 'source_offsets');
403
+ if (source_offsets_attr && extract_static_boolean(source_offsets_attr.value) !== false) {
404
+ return;
405
+ }
406
+
400
407
  // Collect attributes to exclude from precompiled output
401
408
  const exclude_attrs: Set<AST.Attribute> = new Set([content_attr]);
402
409
  if (base_attr) exclude_attrs.add(base_attr);
410
+ if (source_offsets_attr) exclude_attrs.add(source_offsets_attr);
403
411
 
404
412
  // Extract static string value
405
413
  const content_value = extract_static_string(content_attr.value, context.bindings);
@@ -20,11 +20,12 @@ const format_reference = (ref: string): string => (mdz_is_url(ref) ? ref : `\`${
20
20
  * - `{@link url|text}` → `[text](url)` (markdown link, TSDoc canonical form)
21
21
  * - `{@link url text}` → `[text](url)` (TS-lenient space-separated form)
22
22
  * - `{@link https://...}` → `https://...` (bare URL, auto-linked by mdz)
23
- * - `{@link identifier}` → `` `identifier` `` (code formatting)
23
+ * - `{@link identifier}` → the identifier as a code span
24
24
  * - Bare URLs → returned as-is
25
25
  * - Bare markdown links (`[text](url)` ...) → returned as-is
26
+ * - Content that opens with a backtick (a code span, then description text) → returned as-is
26
27
  * - Bare identifiers → wrapped in backticks
27
- * - `identifier description text` → `` `identifier` description text `` (first token is the reference)
28
+ * - `identifier description text` → the first token wrapped in backticks, the rest as written
28
29
  *
29
30
  * @param content - raw `@see` tag content in TSDoc format
30
31
  * @returns mdz-formatted string ready for `Mdz` component
@@ -40,11 +41,14 @@ const format_reference = (ref: string): string => (mdz_is_url(ref) ? ref : `\`${
40
41
  * mdz_from_tsdoc('https://fuz.dev')
41
42
  * // → 'https://fuz.dev'
42
43
  *
43
- * mdz_from_tsdoc('[svelte-docinfo](https://github.com/ryanatkn/svelte-docinfo) for the analysis library')
44
- * // → '[svelte-docinfo](https://github.com/ryanatkn/svelte-docinfo) for the analysis library'
44
+ * mdz_from_tsdoc('[svelte-docinfo](https://github.com/fuzdev/svelte-docinfo) for the analysis library')
45
+ * // → '[svelte-docinfo](https://github.com/fuzdev/svelte-docinfo) for the analysis library'
45
46
  *
46
47
  * mdz_from_tsdoc('tome.ts for the documentation system')
47
48
  * // → '`tome.ts` for the documentation system'
49
+ *
50
+ * mdz_from_tsdoc('`tome.ts` for the `Tome` type')
51
+ * // → '`tome.ts` for the `Tome` type'
48
52
  * ```
49
53
  */
50
54
  export const mdz_from_tsdoc = (content: string): string => {
@@ -85,6 +89,13 @@ export const mdz_from_tsdoc = (content: string): string => {
85
89
  return trimmed;
86
90
  }
87
91
 
92
+ // Pass through content that opens with a backtick: a leading code span is already mdz,
93
+ // and wrapping it again would leave both spans literal. An unclosed one is returned as
94
+ // written too, since adding a backtick the author didn't write can't make it a span.
95
+ if (trimmed.charCodeAt(0) === 96 /* ` */) {
96
+ return trimmed;
97
+ }
98
+
88
99
  // Split at first whitespace: first token is the reference, rest is description
89
100
  const space_index = trimmed.indexOf(' ');
90
101
  if (space_index === -1) {