@fuzdev/fuz_ui 0.185.2 → 0.186.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/dist/mdz.js CHANGED
@@ -28,55 +28,12 @@
28
28
  *
29
29
  * @module
30
30
  */
31
+ import { BACKTICK, ASTERISK, UNDERSCORE, TILDE, NEWLINE, HYPHEN, HASH, SPACE, TAB, LEFT_ANGLE, RIGHT_ANGLE, SLASH, LEFT_BRACKET, RIGHT_BRACKET, LEFT_PAREN, RIGHT_PAREN, A_UPPER, Z_UPPER, HR_HYPHEN_COUNT, MIN_CODEBLOCK_BACKTICKS, MAX_HEADING_LEVEL, HTTPS_PREFIX_LENGTH, HTTP_PREFIX_LENGTH, is_letter, is_tag_name_char, is_word_char, PERIOD, is_valid_path_char, trim_trailing_punctuation, is_at_absolute_path, is_at_relative_path, extract_single_tag, } from './mdz_helpers.js';
31
32
  // TODO design incremental parsing or some system that preserves Svelte components across re-renders when possible
32
33
  /**
33
34
  * Parses text to an array of `MdzNode`.
34
35
  */
35
36
  export const mdz_parse = (text) => new MdzParser(text).parse();
36
- // Character codes for performance
37
- const BACKTICK = 96; // `
38
- const ASTERISK = 42; // *
39
- const UNDERSCORE = 95; // _
40
- const TILDE = 126; // ~
41
- const NEWLINE = 10; // \n
42
- const HYPHEN = 45; // -
43
- const HASH = 35; // #
44
- const SPACE = 32; // (space)
45
- const TAB = 9; // \t
46
- const LEFT_ANGLE = 60; // <
47
- const RIGHT_ANGLE = 62; // >
48
- const SLASH = 47; // /
49
- const LEFT_BRACKET = 91; // [
50
- const RIGHT_BRACKET = 93; // ]
51
- const LEFT_PAREN = 40; // (
52
- const RIGHT_PAREN = 41; // )
53
- const COLON = 58; // :
54
- const PERIOD = 46; // .
55
- const COMMA = 44; // ,
56
- const SEMICOLON = 59; // ;
57
- const EXCLAMATION = 33; // !
58
- const QUESTION = 63; // ?
59
- // RFC 3986 URI characters
60
- const DOLLAR = 36; // $
61
- const PERCENT = 37; // %
62
- const AMPERSAND = 38; // &
63
- const APOSTROPHE = 39; // '
64
- const PLUS = 43; // +
65
- const EQUALS = 61; // =
66
- const AT = 64; // @
67
- // Character ranges
68
- const A_UPPER = 65; // A
69
- const Z_UPPER = 90; // Z
70
- const A_LOWER = 97; // a
71
- const Z_LOWER = 122; // z
72
- const ZERO = 48; // 0
73
- const NINE = 57; // 9
74
- // mdz specification constants
75
- const HR_HYPHEN_COUNT = 3; // Horizontal rule requires exactly 3 hyphens
76
- const MIN_CODEBLOCK_BACKTICKS = 3; // Code blocks require minimum 3 backticks
77
- const MAX_HEADING_LEVEL = 6; // Headings support levels 1-6
78
- const HTTPS_PREFIX_LENGTH = 8; // Length of "https://"
79
- const HTTP_PREFIX_LENGTH = 7; // Length of "http://"
80
37
  /**
81
38
  * Parser for mdz format.
82
39
  * Single-pass lexer/parser with text accumulation for efficiency.
@@ -106,9 +63,9 @@ export class MdzParser {
106
63
  // Skip leading newlines
107
64
  this.#skip_newlines();
108
65
  while (this.#index < this.#template.length) {
109
- // Peek for block element (read-only match), flush paragraph first, then parse.
110
- // Flush must happen before parse because parse modifies accumulation state.
111
- const block_type = this.#peek_block_element();
66
+ // Block elements only start at column 0 skip peek for mid-line characters
67
+ const block_type = (this.#index === 0 || this.#template.charCodeAt(this.#index - 1) === NEWLINE) &&
68
+ this.#peek_block_element();
112
69
  if (block_type) {
113
70
  const flushed = this.#flush_paragraph(paragraph_children, true);
114
71
  if (flushed)
@@ -187,7 +144,7 @@ export class MdzParser {
187
144
  }
188
145
  }
189
146
  // Single tag (component/element) - add directly without paragraph wrapper (MDX convention)
190
- const single_tag = this.#extract_single_tag(paragraph_children);
147
+ const single_tag = extract_single_tag(paragraph_children);
191
148
  if (single_tag) {
192
149
  paragraph_children.length = 0;
193
150
  return single_tag;
@@ -249,7 +206,7 @@ export class MdzParser {
249
206
  * Uses switch for performance (avoids regex in hot loop).
250
207
  */
251
208
  #parse_node() {
252
- const char_code = this.#current_char();
209
+ const char_code = this.#template.charCodeAt(this.#index);
253
210
  // Use character codes for performance in hot path
254
211
  switch (char_code) {
255
212
  case BACKTICK:
@@ -511,7 +468,7 @@ export class MdzParser {
511
468
  // Follows GFM behavior: invalid chars cause fallback to text, then auto-detection
512
469
  for (let i = 0; i < reference.length; i++) {
513
470
  const char_code = reference.charCodeAt(i);
514
- if (!this.#is_valid_path_char(char_code)) {
471
+ if (!is_valid_path_char(char_code)) {
515
472
  // Invalid character in URL, treat as text and let auto-detection handle it
516
473
  this.#index = start + 1;
517
474
  return {
@@ -584,7 +541,7 @@ export class MdzParser {
584
541
  };
585
542
  }
586
543
  const first_char = this.#template.charCodeAt(this.#index);
587
- if (!this.#is_letter(first_char)) {
544
+ if (!is_letter(first_char)) {
588
545
  // Not a valid tag, treat as text - restore parent state
589
546
  this.#restore_accumulation_state(saved_state);
590
547
  return {
@@ -597,7 +554,7 @@ export class MdzParser {
597
554
  // Collect tag name (letters, numbers, hyphens, underscores)
598
555
  while (this.#index < this.#template.length) {
599
556
  const char_code = this.#template.charCodeAt(this.#index);
600
- if (this.#is_tag_name_char(char_code)) {
557
+ if (is_tag_name_char(char_code)) {
601
558
  this.#index++;
602
559
  }
603
560
  else {
@@ -699,35 +656,10 @@ export class MdzParser {
699
656
  end: this.#index,
700
657
  };
701
658
  }
702
- /**
703
- * Extract a single tag (component or element) if it's the only non-whitespace content.
704
- * Returns the tag node if paragraph wrapping should be skipped (MDX convention),
705
- * or null if the content should be wrapped in a paragraph.
706
- */
707
- #extract_single_tag(nodes) {
708
- let tag = null;
709
- for (const node of nodes) {
710
- if (node.type === 'Component' || node.type === 'Element') {
711
- if (tag)
712
- return null; // Multiple tags
713
- tag = node;
714
- }
715
- else if (node.type === 'Text') {
716
- // Allow only whitespace-only text nodes
717
- if (node.content.trim() !== '')
718
- return null;
719
- }
720
- else {
721
- // Any other node type means not a single tag
722
- return null;
723
- }
724
- }
725
- return tag;
726
- }
727
659
  /**
728
660
  * Read-only check if current position matches a block element.
729
661
  * Does not modify parser state — used to peek before flushing paragraph.
730
- * Returns which block type matched, or null if none.
662
+ * Caller must verify column-0 position before calling.
731
663
  */
732
664
  #peek_block_element() {
733
665
  if (this.#match_heading())
@@ -761,44 +693,6 @@ export class MdzParser {
761
693
  this.#nodes.length = 0;
762
694
  this.#nodes.push(...state.nodes);
763
695
  }
764
- /**
765
- * Check if character code is a letter (A-Z, a-z).
766
- */
767
- #is_letter(char_code) {
768
- return ((char_code >= A_UPPER && char_code <= Z_UPPER) ||
769
- (char_code >= A_LOWER && char_code <= Z_LOWER));
770
- }
771
- /**
772
- * Check if character code is valid for tag name (letter, number, hyphen, underscore).
773
- */
774
- #is_tag_name_char(char_code) {
775
- return (this.#is_letter(char_code) ||
776
- (char_code >= ZERO && char_code <= NINE) ||
777
- char_code === HYPHEN ||
778
- char_code === UNDERSCORE);
779
- }
780
- /**
781
- * Check if character is part of a word for word boundary detection.
782
- * Used to prevent intraword emphasis with `_` and `~` delimiters.
783
- *
784
- * Formatting delimiters (`*`, `_`, `~`) are NOT word characters - they're transparent.
785
- * Only alphanumeric characters (A-Z, a-z, 0-9) are considered word characters.
786
- *
787
- * This prevents false positives with snake_case identifiers while allowing
788
- * adjacent formatting like `**bold**_italic_`.
789
- *
790
- * @param char_code - Character code to check
791
- */
792
- #is_word_char(char_code) {
793
- // Formatting delimiters are never word chars (transparent for boundary checks)
794
- if (char_code === ASTERISK || char_code === UNDERSCORE || char_code === TILDE) {
795
- return false;
796
- }
797
- // Alphanumeric characters are word chars for all delimiters
798
- return ((char_code >= A_UPPER && char_code <= Z_UPPER) ||
799
- (char_code >= A_LOWER && char_code <= Z_LOWER) ||
800
- (char_code >= ZERO && char_code <= NINE));
801
- }
802
696
  /**
803
697
  * Check if position is at a word boundary.
804
698
  * Word boundary = not surrounded by word characters (A-Z, a-z, 0-9).
@@ -812,61 +706,19 @@ export class MdzParser {
812
706
  if (check_before && index > 0) {
813
707
  const prev = this.#template.charCodeAt(index - 1);
814
708
  // If preceded by word char, not at boundary
815
- if (this.#is_word_char(prev)) {
709
+ if (is_word_char(prev)) {
816
710
  return false;
817
711
  }
818
712
  }
819
713
  if (check_after && index < this.#template.length) {
820
714
  const next = this.#template.charCodeAt(index);
821
715
  // If followed by word char, not at boundary
822
- if (this.#is_word_char(next)) {
716
+ if (is_word_char(next)) {
823
717
  return false;
824
718
  }
825
719
  }
826
720
  return true;
827
721
  }
828
- /**
829
- * Check if character code is valid in URI path per RFC 3986.
830
- * Validates against the `pchar` production plus path/query/fragment separators.
831
- *
832
- * Valid characters:
833
- * - unreserved: A-Z a-z 0-9 - . _ ~
834
- * - sub-delims: ! $ & ' ( ) * + , ; =
835
- * - path allowed: : @
836
- * - separators: / ? #
837
- * - percent-encoding: %
838
- */
839
- #is_valid_path_char(char_code) {
840
- return ((char_code >= A_UPPER && char_code <= Z_UPPER) ||
841
- (char_code >= A_LOWER && char_code <= Z_LOWER) ||
842
- (char_code >= ZERO && char_code <= NINE) ||
843
- // unreserved: - . _ ~
844
- char_code === HYPHEN ||
845
- char_code === PERIOD ||
846
- char_code === UNDERSCORE ||
847
- char_code === TILDE ||
848
- // sub-delims: ! $ & ' ( ) * + , ; =
849
- char_code === EXCLAMATION ||
850
- char_code === DOLLAR ||
851
- char_code === AMPERSAND ||
852
- char_code === APOSTROPHE ||
853
- char_code === LEFT_PAREN ||
854
- char_code === RIGHT_PAREN ||
855
- char_code === ASTERISK ||
856
- char_code === PLUS ||
857
- char_code === COMMA ||
858
- char_code === SEMICOLON ||
859
- char_code === EQUALS ||
860
- // path allowed: : @
861
- char_code === COLON ||
862
- char_code === AT ||
863
- // separators: / ? #
864
- char_code === SLASH ||
865
- char_code === QUESTION ||
866
- char_code === HASH ||
867
- // percent-encoding: %
868
- char_code === PERCENT);
869
- }
870
722
  /**
871
723
  * Check if current position is the start of an external URL (`https://` or `http://`).
872
724
  */
@@ -891,30 +743,6 @@ export class MdzParser {
891
743
  }
892
744
  return false;
893
745
  }
894
- /**
895
- * Check if current position is the start of an internal path (starts with /).
896
- */
897
- #is_at_internal_path() {
898
- if (this.#template.charCodeAt(this.#index) !== SLASH) {
899
- return false;
900
- }
901
- // Check previous character - must be whitespace or start of string
902
- // (to avoid matching / within relative paths like ./a/b or ../a/b)
903
- if (this.#index > 0) {
904
- const prev_char = this.#template.charCodeAt(this.#index - 1);
905
- if (prev_char !== SPACE && prev_char !== NEWLINE && prev_char !== TAB) {
906
- return false;
907
- }
908
- }
909
- // Must have at least one more character after /, and it must NOT be:
910
- // - another / (to avoid matching // which is used for comments or protocol-relative URLs)
911
- // - whitespace (a bare / followed by space is not a useful link)
912
- if (this.#index + 1 >= this.#template.length) {
913
- return false;
914
- }
915
- const next_char = this.#template.charCodeAt(this.#index + 1);
916
- return next_char !== SLASH && next_char !== SPACE && next_char !== NEWLINE;
917
- }
918
746
  /**
919
747
  * Parse auto-detected external URL (`https://` or `http://`).
920
748
  * Uses RFC 3986 whitelist validation for valid URI characters.
@@ -931,15 +759,15 @@ export class MdzParser {
931
759
  // Collect URL characters using RFC 3986 whitelist
932
760
  // Stop at whitespace or any character invalid in URIs
933
761
  while (this.#index < this.#template.length) {
934
- const char_code = this.#current_char();
935
- if (char_code === SPACE || char_code === NEWLINE || !this.#is_valid_path_char(char_code)) {
762
+ const char_code = this.#template.charCodeAt(this.#index);
763
+ if (char_code === SPACE || char_code === NEWLINE || !is_valid_path_char(char_code)) {
936
764
  break;
937
765
  }
938
766
  this.#index++;
939
767
  }
940
768
  let reference = this.#template.slice(start, this.#index);
941
769
  // Apply GFM trailing punctuation trimming with balanced parentheses
942
- reference = this.#trim_trailing_punctuation(reference);
770
+ reference = trim_trailing_punctuation(reference);
943
771
  // Update index after trimming
944
772
  this.#index = start + reference.length;
945
773
  return {
@@ -952,23 +780,23 @@ export class MdzParser {
952
780
  };
953
781
  }
954
782
  /**
955
- * Parse auto-detected internal path (starts with /).
783
+ * Parse auto-detected path (absolute `/`, relative `./` or `../`).
956
784
  * Uses RFC 3986 whitelist validation for valid URI characters.
957
785
  */
958
- #parse_auto_link_internal() {
786
+ #parse_auto_link_path() {
959
787
  const start = this.#index;
960
788
  // Collect path characters using RFC 3986 whitelist
961
789
  // Stop at whitespace or any character invalid in URIs
962
790
  while (this.#index < this.#template.length) {
963
- const char_code = this.#current_char();
964
- if (char_code === SPACE || char_code === NEWLINE || !this.#is_valid_path_char(char_code)) {
791
+ const char_code = this.#template.charCodeAt(this.#index);
792
+ if (char_code === SPACE || char_code === NEWLINE || !is_valid_path_char(char_code)) {
965
793
  break;
966
794
  }
967
795
  this.#index++;
968
796
  }
969
797
  let reference = this.#template.slice(start, this.#index);
970
798
  // Apply GFM trailing punctuation trimming
971
- reference = this.#trim_trailing_punctuation(reference);
799
+ reference = trim_trailing_punctuation(reference);
972
800
  // Update index after trimming
973
801
  this.#index = start + reference.length;
974
802
  return {
@@ -980,57 +808,6 @@ export class MdzParser {
980
808
  end: this.#index,
981
809
  };
982
810
  }
983
- /**
984
- * Trim trailing punctuation from URL/path per RFC 3986 and GFM rules.
985
- * - Trims simple trailing: .,;:!?]
986
- * - Balanced logic for () only (valid in path components)
987
- * - Invalid chars like [] {} are already stopped by whitelist, but ] trimmed as fallback
988
- *
989
- * Optimized to avoid O(n²) string slicing - tracks end index and slices once at the end.
990
- */
991
- #trim_trailing_punctuation(url) {
992
- let end = url.length;
993
- // Trim simple trailing punctuation (] as fallback - whitelist should prevent it)
994
- while (end > 0) {
995
- const last_char = url.charCodeAt(end - 1);
996
- if (last_char === PERIOD ||
997
- last_char === COMMA ||
998
- last_char === SEMICOLON ||
999
- last_char === COLON ||
1000
- last_char === EXCLAMATION ||
1001
- last_char === QUESTION ||
1002
- last_char === RIGHT_BRACKET) {
1003
- end--;
1004
- }
1005
- else {
1006
- break;
1007
- }
1008
- }
1009
- // Handle balanced parentheses ONLY (parens are valid in URI path components)
1010
- // Count parentheses in the trimmed portion
1011
- let open_count = 0;
1012
- let close_count = 0;
1013
- for (let i = 0; i < end; i++) {
1014
- const char = url.charCodeAt(i);
1015
- if (char === LEFT_PAREN)
1016
- open_count++;
1017
- if (char === RIGHT_PAREN)
1018
- close_count++;
1019
- }
1020
- // Trim unmatched trailing closing parens
1021
- while (end > 0 && close_count > open_count) {
1022
- const last_char = url.charCodeAt(end - 1);
1023
- if (last_char === RIGHT_PAREN) {
1024
- end--;
1025
- close_count--;
1026
- }
1027
- else {
1028
- break;
1029
- }
1030
- }
1031
- // Return original string if no trimming, otherwise slice once
1032
- return end === url.length ? url : url.slice(0, end);
1033
- }
1034
811
  /**
1035
812
  * Parse plain text until special character encountered.
1036
813
  * Preserves all whitespace (except paragraph breaks handled separately).
@@ -1038,15 +815,16 @@ export class MdzParser {
1038
815
  */
1039
816
  #parse_text() {
1040
817
  const start = this.#index;
1041
- // Check for URL or internal path at current position
818
+ // Check for URL or internal absolute/relative path at current position
1042
819
  if (this.#is_at_url()) {
1043
820
  return this.#parse_auto_link_url();
1044
821
  }
1045
- if (this.#is_at_internal_path()) {
1046
- return this.#parse_auto_link_internal();
822
+ if (is_at_absolute_path(this.#template, this.#index) ||
823
+ is_at_relative_path(this.#template, this.#index)) {
824
+ return this.#parse_auto_link_path();
1047
825
  }
1048
826
  while (this.#index < this.#template.length) {
1049
- const char_code = this.#current_char();
827
+ const char_code = this.#template.charCodeAt(this.#index);
1050
828
  // Stop at special characters (but preserve single newlines)
1051
829
  if (char_code === BACKTICK ||
1052
830
  char_code === ASTERISK ||
@@ -1076,8 +854,10 @@ export class MdzParser {
1076
854
  }
1077
855
  }
1078
856
  }
1079
- // Check for URL or internal path mid-text
1080
- if (this.#is_at_url() || this.#is_at_internal_path()) {
857
+ // Check for URL or internal absolute/relative path mid-text (char code guard avoids startsWith on every char)
858
+ if ((char_code === 104 /* h */ && this.#is_at_url()) ||
859
+ (char_code === SLASH && is_at_absolute_path(this.#template, this.#index)) ||
860
+ (char_code === PERIOD && is_at_relative_path(this.#template, this.#index))) {
1081
861
  break;
1082
862
  }
1083
863
  this.#index++;
@@ -1136,18 +916,12 @@ export class MdzParser {
1136
916
  this.#max_search_index = saved_max_search_index;
1137
917
  return nodes;
1138
918
  }
1139
- /**
1140
- * Get character code at current index, or -1 if at EOF.
1141
- */
1142
- #current_char() {
1143
- return this.#index < this.#template.length ? this.#template.charCodeAt(this.#index) : -1;
1144
- }
1145
919
  /**
1146
920
  * Check if current position is at a paragraph break (double newline).
1147
921
  */
1148
922
  #is_at_paragraph_break() {
1149
- return (this.#current_char() === NEWLINE &&
1150
- this.#index + 1 < this.#template.length &&
923
+ return (this.#index + 1 < this.#template.length &&
924
+ this.#template.charCodeAt(this.#index) === NEWLINE &&
1151
925
  this.#template.charCodeAt(this.#index + 1) === NEWLINE);
1152
926
  }
1153
927
  #match(str) {
@@ -1173,10 +947,6 @@ export class MdzParser {
1173
947
  */
1174
948
  #match_hr() {
1175
949
  let i = this.#index;
1176
- // Must start at column 0 (beginning of input or after newline)
1177
- if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
1178
- return false;
1179
- }
1180
950
  // Must have exactly three hyphens
1181
951
  if (i + HR_HYPHEN_COUNT > this.#template.length ||
1182
952
  this.#template.charCodeAt(i) !== HYPHEN ||
@@ -1226,10 +996,6 @@ export class MdzParser {
1226
996
  */
1227
997
  #match_heading() {
1228
998
  let i = this.#index;
1229
- // Must start at column 0 (beginning of input or after newline)
1230
- if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
1231
- return false;
1232
- }
1233
999
  // Count hashes (must be 1-6)
1234
1000
  let hash_count = 0;
1235
1001
  while (i < this.#template.length &&
@@ -1246,20 +1012,17 @@ export class MdzParser {
1246
1012
  return false;
1247
1013
  }
1248
1014
  i++; // consume the space
1249
- // Must have non-whitespace content after the space (not just whitespace until newline)
1250
- let has_content = false;
1251
- while (i < this.#template.length && this.#template.charCodeAt(i) !== NEWLINE) {
1015
+ // Must have at least one non-whitespace character after the space
1016
+ while (i < this.#template.length) {
1252
1017
  const char_code = this.#template.charCodeAt(i);
1253
- if (char_code !== SPACE && char_code !== TAB) {
1254
- has_content = true;
1255
- }
1018
+ if (char_code === NEWLINE)
1019
+ return false; // reached end of line with only whitespace
1020
+ if (char_code !== SPACE && char_code !== TAB)
1021
+ return true;
1256
1022
  i++;
1257
1023
  }
1258
- if (!has_content) {
1259
- return false; // heading with only whitespace, treat as plain text
1260
- }
1261
- // At newline or EOF — both are valid
1262
- return true;
1024
+ // Reached EOF with only whitespace after hashes
1025
+ return false;
1263
1026
  }
1264
1027
  /**
1265
1028
  * Parse heading: `# Heading text`
@@ -1325,10 +1088,6 @@ export class MdzParser {
1325
1088
  */
1326
1089
  #match_code_block() {
1327
1090
  let i = this.#index;
1328
- // Must start at column 0 (beginning of input or after newline)
1329
- if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
1330
- return false;
1331
- }
1332
1091
  // Must have at least three backticks
1333
1092
  let backtick_count = 0;
1334
1093
  while (i < this.#template.length && this.#template.charCodeAt(i) === BACKTICK) {
@@ -1482,3 +1241,37 @@ export class MdzParser {
1482
1241
  */
1483
1242
  const URL_PATTERN = /^https?:\/\/[^\s)\]}<>.,:/?#!]/;
1484
1243
  export const mdz_is_url = (s) => URL_PATTERN.test(s);
1244
+ /**
1245
+ * Resolves a relative path (`./` or `../`) against a base path.
1246
+ * The base is treated as a directory regardless of trailing slash
1247
+ * (`'/docs/mdz'` and `'/docs/mdz/'` behave identically).
1248
+ * Handles embedded `.` and `..` segments within the reference
1249
+ * (e.g., `'./a/../b'` → navigates up then down).
1250
+ * Clamps at root — excess `..` segments stop at `/` rather than escaping.
1251
+ *
1252
+ * @param reference A relative path starting with `./` or `../`.
1253
+ * @param base An absolute base path (e.g., `'/docs/mdz/'`). Empty string is treated as root.
1254
+ * @returns An absolute resolved path (e.g., `'/docs/mdz/grammar'`).
1255
+ */
1256
+ export const resolve_relative_path = (reference, base) => {
1257
+ const segments = base.split('/');
1258
+ // Remove trailing empty from split (e.g., '/docs/mdz/' → ['', 'docs', 'mdz', ''])
1259
+ // but keep the root segment ([''] from '' base or ['', ''] from '/').
1260
+ if (segments.length > 1 && segments.at(-1) === '')
1261
+ segments.pop();
1262
+ const trailing = reference.endsWith('/');
1263
+ for (const segment of reference.split('/')) {
1264
+ if (segment === '.' || segment === '')
1265
+ continue;
1266
+ if (segment === '..') {
1267
+ if (segments.length > 1)
1268
+ segments.pop(); // clamp at root
1269
+ }
1270
+ else {
1271
+ segments.push(segment);
1272
+ }
1273
+ }
1274
+ if (trailing)
1275
+ segments.push('');
1276
+ return segments.join('/');
1277
+ };
@@ -34,4 +34,16 @@ export declare const mdz_elements_context: {
34
34
  get_maybe: () => MdzElements | undefined;
35
35
  set: (value: MdzElements) => MdzElements;
36
36
  };
37
+ /**
38
+ * Context for providing a base path getter for resolving relative links in mdz content.
39
+ * Set to a getter (e.g., `() => base`) so changes to the base prop are reflected
40
+ * without needing an effect. When the getter returns a path like `'/docs/mdz/'`,
41
+ * relative paths like `./grammar` resolve to `/docs/mdz/grammar` before rendering.
42
+ * When not set, relative paths use raw hrefs (browser resolves them).
43
+ */
44
+ export declare const mdz_base_context: {
45
+ get: (error_message?: string) => () => string | undefined;
46
+ get_maybe: () => (() => string | undefined) | undefined;
47
+ set: (value: () => string | undefined) => () => string | undefined;
48
+ };
37
49
  //# sourceMappingURL=mdz_components.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mdz_components.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_components.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,QAAQ,CAAC;AAItC;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAE7D;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,sBAAsB;;;;CAAkC,CAAC;AAEtE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;CAAgC,CAAC"}
1
+ {"version":3,"file":"mdz_components.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_components.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,QAAQ,CAAC;AAItC;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAE7D;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,sBAAsB;;;;CAAkC,CAAC;AAEtE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;CAAgC,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB;2CAAwB,MAAM,GAAG,SAAS;4BAAlB,MAAM,GAAG,SAAS;uBAAlB,MAAM,GAAG,SAAS,WAAlB,MAAM,GAAG,SAAS;CAAG,CAAC"}
@@ -10,3 +10,11 @@ export const mdz_components_context = create_context();
10
10
  * By default, no HTML elements are allowed.
11
11
  */
12
12
  export const mdz_elements_context = create_context();
13
+ /**
14
+ * Context for providing a base path getter for resolving relative links in mdz content.
15
+ * Set to a getter (e.g., `() => base`) so changes to the base prop are reflected
16
+ * without needing an effect. When the getter returns a path like `'/docs/mdz/'`,
17
+ * relative paths like `./grammar` resolve to `/docs/mdz/grammar` before rendering.
18
+ * When not set, relative paths use raw hrefs (browser resolves them).
19
+ */
20
+ export const mdz_base_context = create_context();