@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.svelte +5 -0
- package/dist/Mdz.svelte.d.ts +1 -0
- package/dist/Mdz.svelte.d.ts.map +1 -1
- package/dist/MdzNodeView.svelte +19 -8
- package/dist/MdzNodeView.svelte.d.ts +1 -1
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/mdz.d.ts +13 -0
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +73 -280
- package/dist/mdz_components.d.ts +12 -0
- package/dist/mdz_components.d.ts.map +1 -1
- package/dist/mdz_components.js +8 -0
- package/dist/mdz_helpers.d.ts +108 -0
- package/dist/mdz_helpers.d.ts.map +1 -0
- package/dist/mdz_helpers.js +237 -0
- package/dist/mdz_lexer.d.ts +93 -0
- package/dist/mdz_lexer.d.ts.map +1 -0
- package/dist/mdz_lexer.js +727 -0
- package/dist/mdz_to_svelte.d.ts +5 -2
- package/dist/mdz_to_svelte.d.ts.map +1 -1
- package/dist/mdz_to_svelte.js +13 -2
- package/dist/mdz_token_parser.d.ts +14 -0
- package/dist/mdz_token_parser.d.ts.map +1 -0
- package/dist/mdz_token_parser.js +374 -0
- package/dist/svelte_preprocess_mdz.js +23 -7
- package/package.json +3 -2
- package/src/lib/mdz.ts +106 -302
- package/src/lib/mdz_components.ts +9 -0
- package/src/lib/mdz_helpers.ts +251 -0
- package/src/lib/mdz_lexer.ts +1003 -0
- package/src/lib/mdz_to_svelte.ts +15 -2
- package/src/lib/mdz_token_parser.ts +460 -0
- package/src/lib/svelte_preprocess_mdz.ts +23 -7
package/src/lib/mdz.ts
CHANGED
|
@@ -29,6 +29,41 @@
|
|
|
29
29
|
* @module
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
import {
|
|
33
|
+
BACKTICK,
|
|
34
|
+
ASTERISK,
|
|
35
|
+
UNDERSCORE,
|
|
36
|
+
TILDE,
|
|
37
|
+
NEWLINE,
|
|
38
|
+
HYPHEN,
|
|
39
|
+
HASH,
|
|
40
|
+
SPACE,
|
|
41
|
+
TAB,
|
|
42
|
+
LEFT_ANGLE,
|
|
43
|
+
RIGHT_ANGLE,
|
|
44
|
+
SLASH,
|
|
45
|
+
LEFT_BRACKET,
|
|
46
|
+
RIGHT_BRACKET,
|
|
47
|
+
LEFT_PAREN,
|
|
48
|
+
RIGHT_PAREN,
|
|
49
|
+
A_UPPER,
|
|
50
|
+
Z_UPPER,
|
|
51
|
+
HR_HYPHEN_COUNT,
|
|
52
|
+
MIN_CODEBLOCK_BACKTICKS,
|
|
53
|
+
MAX_HEADING_LEVEL,
|
|
54
|
+
HTTPS_PREFIX_LENGTH,
|
|
55
|
+
HTTP_PREFIX_LENGTH,
|
|
56
|
+
is_letter,
|
|
57
|
+
is_tag_name_char,
|
|
58
|
+
is_word_char,
|
|
59
|
+
PERIOD,
|
|
60
|
+
is_valid_path_char,
|
|
61
|
+
trim_trailing_punctuation,
|
|
62
|
+
is_at_absolute_path,
|
|
63
|
+
is_at_relative_path,
|
|
64
|
+
extract_single_tag,
|
|
65
|
+
} from './mdz_helpers.js';
|
|
66
|
+
|
|
32
67
|
// TODO design incremental parsing or some system that preserves Svelte components across re-renders when possible
|
|
33
68
|
|
|
34
69
|
/**
|
|
@@ -91,7 +126,7 @@ export interface MdzLinkNode extends MdzBaseNode {
|
|
|
91
126
|
type: 'Link';
|
|
92
127
|
reference: string; // URL or path
|
|
93
128
|
children: Array<MdzNode>; // Display content (can include inline formatting)
|
|
94
|
-
link_type: 'external' | 'internal'; // external: https/http, internal: /path
|
|
129
|
+
link_type: 'external' | 'internal'; // external: https/http, internal: /path, ./path, ../path
|
|
95
130
|
}
|
|
96
131
|
|
|
97
132
|
export interface MdzParagraphNode extends MdzBaseNode {
|
|
@@ -121,51 +156,6 @@ export interface MdzComponentNode extends MdzBaseNode {
|
|
|
121
156
|
children: Array<MdzNode>;
|
|
122
157
|
}
|
|
123
158
|
|
|
124
|
-
// Character codes for performance
|
|
125
|
-
const BACKTICK = 96; // `
|
|
126
|
-
const ASTERISK = 42; // *
|
|
127
|
-
const UNDERSCORE = 95; // _
|
|
128
|
-
const TILDE = 126; // ~
|
|
129
|
-
const NEWLINE = 10; // \n
|
|
130
|
-
const HYPHEN = 45; // -
|
|
131
|
-
const HASH = 35; // #
|
|
132
|
-
const SPACE = 32; // (space)
|
|
133
|
-
const TAB = 9; // \t
|
|
134
|
-
const LEFT_ANGLE = 60; // <
|
|
135
|
-
const RIGHT_ANGLE = 62; // >
|
|
136
|
-
const SLASH = 47; // /
|
|
137
|
-
const LEFT_BRACKET = 91; // [
|
|
138
|
-
const RIGHT_BRACKET = 93; // ]
|
|
139
|
-
const LEFT_PAREN = 40; // (
|
|
140
|
-
const RIGHT_PAREN = 41; // )
|
|
141
|
-
const COLON = 58; // :
|
|
142
|
-
const PERIOD = 46; // .
|
|
143
|
-
const COMMA = 44; // ,
|
|
144
|
-
const SEMICOLON = 59; // ;
|
|
145
|
-
const EXCLAMATION = 33; // !
|
|
146
|
-
const QUESTION = 63; // ?
|
|
147
|
-
// RFC 3986 URI characters
|
|
148
|
-
const DOLLAR = 36; // $
|
|
149
|
-
const PERCENT = 37; // %
|
|
150
|
-
const AMPERSAND = 38; // &
|
|
151
|
-
const APOSTROPHE = 39; // '
|
|
152
|
-
const PLUS = 43; // +
|
|
153
|
-
const EQUALS = 61; // =
|
|
154
|
-
const AT = 64; // @
|
|
155
|
-
// Character ranges
|
|
156
|
-
const A_UPPER = 65; // A
|
|
157
|
-
const Z_UPPER = 90; // Z
|
|
158
|
-
const A_LOWER = 97; // a
|
|
159
|
-
const Z_LOWER = 122; // z
|
|
160
|
-
const ZERO = 48; // 0
|
|
161
|
-
const NINE = 57; // 9
|
|
162
|
-
// mdz specification constants
|
|
163
|
-
const HR_HYPHEN_COUNT = 3; // Horizontal rule requires exactly 3 hyphens
|
|
164
|
-
const MIN_CODEBLOCK_BACKTICKS = 3; // Code blocks require minimum 3 backticks
|
|
165
|
-
const MAX_HEADING_LEVEL = 6; // Headings support levels 1-6
|
|
166
|
-
const HTTPS_PREFIX_LENGTH = 8; // Length of "https://"
|
|
167
|
-
const HTTP_PREFIX_LENGTH = 7; // Length of "http://"
|
|
168
|
-
|
|
169
159
|
/**
|
|
170
160
|
* Parser for mdz format.
|
|
171
161
|
* Single-pass lexer/parser with text accumulation for efficiency.
|
|
@@ -199,9 +189,10 @@ export class MdzParser {
|
|
|
199
189
|
this.#skip_newlines();
|
|
200
190
|
|
|
201
191
|
while (this.#index < this.#template.length) {
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
192
|
+
// Block elements only start at column 0 — skip peek for mid-line characters
|
|
193
|
+
const block_type =
|
|
194
|
+
(this.#index === 0 || this.#template.charCodeAt(this.#index - 1) === NEWLINE) &&
|
|
195
|
+
this.#peek_block_element();
|
|
205
196
|
if (block_type) {
|
|
206
197
|
const flushed = this.#flush_paragraph(paragraph_children, true);
|
|
207
198
|
if (flushed) root_nodes.push(flushed);
|
|
@@ -283,7 +274,7 @@ export class MdzParser {
|
|
|
283
274
|
}
|
|
284
275
|
|
|
285
276
|
// Single tag (component/element) - add directly without paragraph wrapper (MDX convention)
|
|
286
|
-
const single_tag =
|
|
277
|
+
const single_tag = extract_single_tag(paragraph_children);
|
|
287
278
|
if (single_tag) {
|
|
288
279
|
paragraph_children.length = 0;
|
|
289
280
|
return single_tag;
|
|
@@ -353,7 +344,7 @@ export class MdzParser {
|
|
|
353
344
|
* Uses switch for performance (avoids regex in hot loop).
|
|
354
345
|
*/
|
|
355
346
|
#parse_node(): MdzNode {
|
|
356
|
-
const char_code = this.#
|
|
347
|
+
const char_code = this.#template.charCodeAt(this.#index);
|
|
357
348
|
|
|
358
349
|
// Use character codes for performance in hot path
|
|
359
350
|
switch (char_code) {
|
|
@@ -683,7 +674,7 @@ export class MdzParser {
|
|
|
683
674
|
// Follows GFM behavior: invalid chars cause fallback to text, then auto-detection
|
|
684
675
|
for (let i = 0; i < reference.length; i++) {
|
|
685
676
|
const char_code = reference.charCodeAt(i);
|
|
686
|
-
if (!
|
|
677
|
+
if (!is_valid_path_char(char_code)) {
|
|
687
678
|
// Invalid character in URL, treat as text and let auto-detection handle it
|
|
688
679
|
this.#index = start + 1;
|
|
689
680
|
return {
|
|
@@ -767,7 +758,7 @@ export class MdzParser {
|
|
|
767
758
|
}
|
|
768
759
|
|
|
769
760
|
const first_char = this.#template.charCodeAt(this.#index);
|
|
770
|
-
if (!
|
|
761
|
+
if (!is_letter(first_char)) {
|
|
771
762
|
// Not a valid tag, treat as text - restore parent state
|
|
772
763
|
this.#restore_accumulation_state(saved_state);
|
|
773
764
|
return {
|
|
@@ -781,7 +772,7 @@ export class MdzParser {
|
|
|
781
772
|
// Collect tag name (letters, numbers, hyphens, underscores)
|
|
782
773
|
while (this.#index < this.#template.length) {
|
|
783
774
|
const char_code = this.#template.charCodeAt(this.#index);
|
|
784
|
-
if (
|
|
775
|
+
if (is_tag_name_char(char_code)) {
|
|
785
776
|
this.#index++;
|
|
786
777
|
} else {
|
|
787
778
|
break;
|
|
@@ -907,34 +898,10 @@ export class MdzParser {
|
|
|
907
898
|
};
|
|
908
899
|
}
|
|
909
900
|
|
|
910
|
-
/**
|
|
911
|
-
* Extract a single tag (component or element) if it's the only non-whitespace content.
|
|
912
|
-
* Returns the tag node if paragraph wrapping should be skipped (MDX convention),
|
|
913
|
-
* or null if the content should be wrapped in a paragraph.
|
|
914
|
-
*/
|
|
915
|
-
#extract_single_tag(nodes: Array<MdzNode>): MdzComponentNode | MdzElementNode | null {
|
|
916
|
-
let tag: MdzComponentNode | MdzElementNode | null = null;
|
|
917
|
-
|
|
918
|
-
for (const node of nodes) {
|
|
919
|
-
if (node.type === 'Component' || node.type === 'Element') {
|
|
920
|
-
if (tag) return null; // Multiple tags
|
|
921
|
-
tag = node;
|
|
922
|
-
} else if (node.type === 'Text') {
|
|
923
|
-
// Allow only whitespace-only text nodes
|
|
924
|
-
if (node.content.trim() !== '') return null;
|
|
925
|
-
} else {
|
|
926
|
-
// Any other node type means not a single tag
|
|
927
|
-
return null;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
return tag;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
901
|
/**
|
|
935
902
|
* Read-only check if current position matches a block element.
|
|
936
903
|
* Does not modify parser state — used to peek before flushing paragraph.
|
|
937
|
-
*
|
|
904
|
+
* Caller must verify column-0 position before calling.
|
|
938
905
|
*/
|
|
939
906
|
#peek_block_element(): 'heading' | 'hr' | 'codeblock' | null {
|
|
940
907
|
if (this.#match_heading()) return 'heading';
|
|
@@ -976,54 +943,6 @@ export class MdzParser {
|
|
|
976
943
|
this.#nodes.push(...state.nodes);
|
|
977
944
|
}
|
|
978
945
|
|
|
979
|
-
/**
|
|
980
|
-
* Check if character code is a letter (A-Z, a-z).
|
|
981
|
-
*/
|
|
982
|
-
#is_letter(char_code: number): boolean {
|
|
983
|
-
return (
|
|
984
|
-
(char_code >= A_UPPER && char_code <= Z_UPPER) ||
|
|
985
|
-
(char_code >= A_LOWER && char_code <= Z_LOWER)
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
/**
|
|
990
|
-
* Check if character code is valid for tag name (letter, number, hyphen, underscore).
|
|
991
|
-
*/
|
|
992
|
-
#is_tag_name_char(char_code: number): boolean {
|
|
993
|
-
return (
|
|
994
|
-
this.#is_letter(char_code) ||
|
|
995
|
-
(char_code >= ZERO && char_code <= NINE) ||
|
|
996
|
-
char_code === HYPHEN ||
|
|
997
|
-
char_code === UNDERSCORE
|
|
998
|
-
);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
/**
|
|
1002
|
-
* Check if character is part of a word for word boundary detection.
|
|
1003
|
-
* Used to prevent intraword emphasis with `_` and `~` delimiters.
|
|
1004
|
-
*
|
|
1005
|
-
* Formatting delimiters (`*`, `_`, `~`) are NOT word characters - they're transparent.
|
|
1006
|
-
* Only alphanumeric characters (A-Z, a-z, 0-9) are considered word characters.
|
|
1007
|
-
*
|
|
1008
|
-
* This prevents false positives with snake_case identifiers while allowing
|
|
1009
|
-
* adjacent formatting like `**bold**_italic_`.
|
|
1010
|
-
*
|
|
1011
|
-
* @param char_code - Character code to check
|
|
1012
|
-
*/
|
|
1013
|
-
#is_word_char(char_code: number): boolean {
|
|
1014
|
-
// Formatting delimiters are never word chars (transparent for boundary checks)
|
|
1015
|
-
if (char_code === ASTERISK || char_code === UNDERSCORE || char_code === TILDE) {
|
|
1016
|
-
return false;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
// Alphanumeric characters are word chars for all delimiters
|
|
1020
|
-
return (
|
|
1021
|
-
(char_code >= A_UPPER && char_code <= Z_UPPER) ||
|
|
1022
|
-
(char_code >= A_LOWER && char_code <= Z_LOWER) ||
|
|
1023
|
-
(char_code >= ZERO && char_code <= NINE)
|
|
1024
|
-
);
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
946
|
/**
|
|
1028
947
|
* Check if position is at a word boundary.
|
|
1029
948
|
* Word boundary = not surrounded by word characters (A-Z, a-z, 0-9).
|
|
@@ -1037,7 +956,7 @@ export class MdzParser {
|
|
|
1037
956
|
if (check_before && index > 0) {
|
|
1038
957
|
const prev = this.#template.charCodeAt(index - 1);
|
|
1039
958
|
// If preceded by word char, not at boundary
|
|
1040
|
-
if (
|
|
959
|
+
if (is_word_char(prev)) {
|
|
1041
960
|
return false;
|
|
1042
961
|
}
|
|
1043
962
|
}
|
|
@@ -1045,7 +964,7 @@ export class MdzParser {
|
|
|
1045
964
|
if (check_after && index < this.#template.length) {
|
|
1046
965
|
const next = this.#template.charCodeAt(index);
|
|
1047
966
|
// If followed by word char, not at boundary
|
|
1048
|
-
if (
|
|
967
|
+
if (is_word_char(next)) {
|
|
1049
968
|
return false;
|
|
1050
969
|
}
|
|
1051
970
|
}
|
|
@@ -1053,51 +972,6 @@ export class MdzParser {
|
|
|
1053
972
|
return true;
|
|
1054
973
|
}
|
|
1055
974
|
|
|
1056
|
-
/**
|
|
1057
|
-
* Check if character code is valid in URI path per RFC 3986.
|
|
1058
|
-
* Validates against the `pchar` production plus path/query/fragment separators.
|
|
1059
|
-
*
|
|
1060
|
-
* Valid characters:
|
|
1061
|
-
* - unreserved: A-Z a-z 0-9 - . _ ~
|
|
1062
|
-
* - sub-delims: ! $ & ' ( ) * + , ; =
|
|
1063
|
-
* - path allowed: : @
|
|
1064
|
-
* - separators: / ? #
|
|
1065
|
-
* - percent-encoding: %
|
|
1066
|
-
*/
|
|
1067
|
-
#is_valid_path_char(char_code: number): boolean {
|
|
1068
|
-
return (
|
|
1069
|
-
(char_code >= A_UPPER && char_code <= Z_UPPER) ||
|
|
1070
|
-
(char_code >= A_LOWER && char_code <= Z_LOWER) ||
|
|
1071
|
-
(char_code >= ZERO && char_code <= NINE) ||
|
|
1072
|
-
// unreserved: - . _ ~
|
|
1073
|
-
char_code === HYPHEN ||
|
|
1074
|
-
char_code === PERIOD ||
|
|
1075
|
-
char_code === UNDERSCORE ||
|
|
1076
|
-
char_code === TILDE ||
|
|
1077
|
-
// sub-delims: ! $ & ' ( ) * + , ; =
|
|
1078
|
-
char_code === EXCLAMATION ||
|
|
1079
|
-
char_code === DOLLAR ||
|
|
1080
|
-
char_code === AMPERSAND ||
|
|
1081
|
-
char_code === APOSTROPHE ||
|
|
1082
|
-
char_code === LEFT_PAREN ||
|
|
1083
|
-
char_code === RIGHT_PAREN ||
|
|
1084
|
-
char_code === ASTERISK ||
|
|
1085
|
-
char_code === PLUS ||
|
|
1086
|
-
char_code === COMMA ||
|
|
1087
|
-
char_code === SEMICOLON ||
|
|
1088
|
-
char_code === EQUALS ||
|
|
1089
|
-
// path allowed: : @
|
|
1090
|
-
char_code === COLON ||
|
|
1091
|
-
char_code === AT ||
|
|
1092
|
-
// separators: / ? #
|
|
1093
|
-
char_code === SLASH ||
|
|
1094
|
-
char_code === QUESTION ||
|
|
1095
|
-
char_code === HASH ||
|
|
1096
|
-
// percent-encoding: %
|
|
1097
|
-
char_code === PERCENT
|
|
1098
|
-
);
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
975
|
/**
|
|
1102
976
|
* Check if current position is the start of an external URL (`https://` or `http://`).
|
|
1103
977
|
*/
|
|
@@ -1123,31 +997,6 @@ export class MdzParser {
|
|
|
1123
997
|
return false;
|
|
1124
998
|
}
|
|
1125
999
|
|
|
1126
|
-
/**
|
|
1127
|
-
* Check if current position is the start of an internal path (starts with /).
|
|
1128
|
-
*/
|
|
1129
|
-
#is_at_internal_path(): boolean {
|
|
1130
|
-
if (this.#template.charCodeAt(this.#index) !== SLASH) {
|
|
1131
|
-
return false;
|
|
1132
|
-
}
|
|
1133
|
-
// Check previous character - must be whitespace or start of string
|
|
1134
|
-
// (to avoid matching / within relative paths like ./a/b or ../a/b)
|
|
1135
|
-
if (this.#index > 0) {
|
|
1136
|
-
const prev_char = this.#template.charCodeAt(this.#index - 1);
|
|
1137
|
-
if (prev_char !== SPACE && prev_char !== NEWLINE && prev_char !== TAB) {
|
|
1138
|
-
return false;
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
// Must have at least one more character after /, and it must NOT be:
|
|
1142
|
-
// - another / (to avoid matching // which is used for comments or protocol-relative URLs)
|
|
1143
|
-
// - whitespace (a bare / followed by space is not a useful link)
|
|
1144
|
-
if (this.#index + 1 >= this.#template.length) {
|
|
1145
|
-
return false;
|
|
1146
|
-
}
|
|
1147
|
-
const next_char = this.#template.charCodeAt(this.#index + 1);
|
|
1148
|
-
return next_char !== SLASH && next_char !== SPACE && next_char !== NEWLINE;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
1000
|
/**
|
|
1152
1001
|
* Parse auto-detected external URL (`https://` or `http://`).
|
|
1153
1002
|
* Uses RFC 3986 whitelist validation for valid URI characters.
|
|
@@ -1165,8 +1014,8 @@ export class MdzParser {
|
|
|
1165
1014
|
// Collect URL characters using RFC 3986 whitelist
|
|
1166
1015
|
// Stop at whitespace or any character invalid in URIs
|
|
1167
1016
|
while (this.#index < this.#template.length) {
|
|
1168
|
-
const char_code = this.#
|
|
1169
|
-
if (char_code === SPACE || char_code === NEWLINE || !
|
|
1017
|
+
const char_code = this.#template.charCodeAt(this.#index);
|
|
1018
|
+
if (char_code === SPACE || char_code === NEWLINE || !is_valid_path_char(char_code)) {
|
|
1170
1019
|
break;
|
|
1171
1020
|
}
|
|
1172
1021
|
this.#index++;
|
|
@@ -1175,7 +1024,7 @@ export class MdzParser {
|
|
|
1175
1024
|
let reference = this.#template.slice(start, this.#index);
|
|
1176
1025
|
|
|
1177
1026
|
// Apply GFM trailing punctuation trimming with balanced parentheses
|
|
1178
|
-
reference =
|
|
1027
|
+
reference = trim_trailing_punctuation(reference);
|
|
1179
1028
|
|
|
1180
1029
|
// Update index after trimming
|
|
1181
1030
|
this.#index = start + reference.length;
|
|
@@ -1191,17 +1040,17 @@ export class MdzParser {
|
|
|
1191
1040
|
}
|
|
1192
1041
|
|
|
1193
1042
|
/**
|
|
1194
|
-
* Parse auto-detected
|
|
1043
|
+
* Parse auto-detected path (absolute `/`, relative `./` or `../`).
|
|
1195
1044
|
* Uses RFC 3986 whitelist validation for valid URI characters.
|
|
1196
1045
|
*/
|
|
1197
|
-
#
|
|
1046
|
+
#parse_auto_link_path(): MdzLinkNode {
|
|
1198
1047
|
const start = this.#index;
|
|
1199
1048
|
|
|
1200
1049
|
// Collect path characters using RFC 3986 whitelist
|
|
1201
1050
|
// Stop at whitespace or any character invalid in URIs
|
|
1202
1051
|
while (this.#index < this.#template.length) {
|
|
1203
|
-
const char_code = this.#
|
|
1204
|
-
if (char_code === SPACE || char_code === NEWLINE || !
|
|
1052
|
+
const char_code = this.#template.charCodeAt(this.#index);
|
|
1053
|
+
if (char_code === SPACE || char_code === NEWLINE || !is_valid_path_char(char_code)) {
|
|
1205
1054
|
break;
|
|
1206
1055
|
}
|
|
1207
1056
|
this.#index++;
|
|
@@ -1210,7 +1059,7 @@ export class MdzParser {
|
|
|
1210
1059
|
let reference = this.#template.slice(start, this.#index);
|
|
1211
1060
|
|
|
1212
1061
|
// Apply GFM trailing punctuation trimming
|
|
1213
|
-
reference =
|
|
1062
|
+
reference = trim_trailing_punctuation(reference);
|
|
1214
1063
|
|
|
1215
1064
|
// Update index after trimming
|
|
1216
1065
|
this.#index = start + reference.length;
|
|
@@ -1225,60 +1074,6 @@ export class MdzParser {
|
|
|
1225
1074
|
};
|
|
1226
1075
|
}
|
|
1227
1076
|
|
|
1228
|
-
/**
|
|
1229
|
-
* Trim trailing punctuation from URL/path per RFC 3986 and GFM rules.
|
|
1230
|
-
* - Trims simple trailing: .,;:!?]
|
|
1231
|
-
* - Balanced logic for () only (valid in path components)
|
|
1232
|
-
* - Invalid chars like [] {} are already stopped by whitelist, but ] trimmed as fallback
|
|
1233
|
-
*
|
|
1234
|
-
* Optimized to avoid O(n²) string slicing - tracks end index and slices once at the end.
|
|
1235
|
-
*/
|
|
1236
|
-
#trim_trailing_punctuation(url: string): string {
|
|
1237
|
-
let end = url.length;
|
|
1238
|
-
|
|
1239
|
-
// Trim simple trailing punctuation (] as fallback - whitelist should prevent it)
|
|
1240
|
-
while (end > 0) {
|
|
1241
|
-
const last_char = url.charCodeAt(end - 1);
|
|
1242
|
-
if (
|
|
1243
|
-
last_char === PERIOD ||
|
|
1244
|
-
last_char === COMMA ||
|
|
1245
|
-
last_char === SEMICOLON ||
|
|
1246
|
-
last_char === COLON ||
|
|
1247
|
-
last_char === EXCLAMATION ||
|
|
1248
|
-
last_char === QUESTION ||
|
|
1249
|
-
last_char === RIGHT_BRACKET
|
|
1250
|
-
) {
|
|
1251
|
-
end--;
|
|
1252
|
-
} else {
|
|
1253
|
-
break;
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// Handle balanced parentheses ONLY (parens are valid in URI path components)
|
|
1258
|
-
// Count parentheses in the trimmed portion
|
|
1259
|
-
let open_count = 0;
|
|
1260
|
-
let close_count = 0;
|
|
1261
|
-
for (let i = 0; i < end; i++) {
|
|
1262
|
-
const char = url.charCodeAt(i);
|
|
1263
|
-
if (char === LEFT_PAREN) open_count++;
|
|
1264
|
-
if (char === RIGHT_PAREN) close_count++;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
// Trim unmatched trailing closing parens
|
|
1268
|
-
while (end > 0 && close_count > open_count) {
|
|
1269
|
-
const last_char = url.charCodeAt(end - 1);
|
|
1270
|
-
if (last_char === RIGHT_PAREN) {
|
|
1271
|
-
end--;
|
|
1272
|
-
close_count--;
|
|
1273
|
-
} else {
|
|
1274
|
-
break;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// Return original string if no trimming, otherwise slice once
|
|
1279
|
-
return end === url.length ? url : url.slice(0, end);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
1077
|
/**
|
|
1283
1078
|
* Parse plain text until special character encountered.
|
|
1284
1079
|
* Preserves all whitespace (except paragraph breaks handled separately).
|
|
@@ -1287,16 +1082,19 @@ export class MdzParser {
|
|
|
1287
1082
|
#parse_text(): MdzTextNode | MdzLinkNode {
|
|
1288
1083
|
const start = this.#index;
|
|
1289
1084
|
|
|
1290
|
-
// Check for URL or internal path at current position
|
|
1085
|
+
// Check for URL or internal absolute/relative path at current position
|
|
1291
1086
|
if (this.#is_at_url()) {
|
|
1292
1087
|
return this.#parse_auto_link_url();
|
|
1293
1088
|
}
|
|
1294
|
-
if (
|
|
1295
|
-
|
|
1089
|
+
if (
|
|
1090
|
+
is_at_absolute_path(this.#template, this.#index) ||
|
|
1091
|
+
is_at_relative_path(this.#template, this.#index)
|
|
1092
|
+
) {
|
|
1093
|
+
return this.#parse_auto_link_path();
|
|
1296
1094
|
}
|
|
1297
1095
|
|
|
1298
1096
|
while (this.#index < this.#template.length) {
|
|
1299
|
-
const char_code = this.#
|
|
1097
|
+
const char_code = this.#template.charCodeAt(this.#index);
|
|
1300
1098
|
|
|
1301
1099
|
// Stop at special characters (but preserve single newlines)
|
|
1302
1100
|
if (
|
|
@@ -1332,8 +1130,12 @@ export class MdzParser {
|
|
|
1332
1130
|
}
|
|
1333
1131
|
}
|
|
1334
1132
|
|
|
1335
|
-
// Check for URL or internal path mid-text
|
|
1336
|
-
if (
|
|
1133
|
+
// Check for URL or internal absolute/relative path mid-text (char code guard avoids startsWith on every char)
|
|
1134
|
+
if (
|
|
1135
|
+
(char_code === 104 /* h */ && this.#is_at_url()) ||
|
|
1136
|
+
(char_code === SLASH && is_at_absolute_path(this.#template, this.#index)) ||
|
|
1137
|
+
(char_code === PERIOD && is_at_relative_path(this.#template, this.#index))
|
|
1138
|
+
) {
|
|
1337
1139
|
break;
|
|
1338
1140
|
}
|
|
1339
1141
|
|
|
@@ -1404,20 +1206,13 @@ export class MdzParser {
|
|
|
1404
1206
|
return nodes;
|
|
1405
1207
|
}
|
|
1406
1208
|
|
|
1407
|
-
/**
|
|
1408
|
-
* Get character code at current index, or -1 if at EOF.
|
|
1409
|
-
*/
|
|
1410
|
-
#current_char(): number {
|
|
1411
|
-
return this.#index < this.#template.length ? this.#template.charCodeAt(this.#index) : -1;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
1209
|
/**
|
|
1415
1210
|
* Check if current position is at a paragraph break (double newline).
|
|
1416
1211
|
*/
|
|
1417
1212
|
#is_at_paragraph_break(): boolean {
|
|
1418
1213
|
return (
|
|
1419
|
-
this.#current_char() === NEWLINE &&
|
|
1420
1214
|
this.#index + 1 < this.#template.length &&
|
|
1215
|
+
this.#template.charCodeAt(this.#index) === NEWLINE &&
|
|
1421
1216
|
this.#template.charCodeAt(this.#index + 1) === NEWLINE
|
|
1422
1217
|
);
|
|
1423
1218
|
}
|
|
@@ -1447,11 +1242,6 @@ export class MdzParser {
|
|
|
1447
1242
|
#match_hr(): boolean {
|
|
1448
1243
|
let i = this.#index;
|
|
1449
1244
|
|
|
1450
|
-
// Must start at column 0 (beginning of input or after newline)
|
|
1451
|
-
if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
|
|
1452
|
-
return false;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
1245
|
// Must have exactly three hyphens
|
|
1456
1246
|
if (
|
|
1457
1247
|
i + HR_HYPHEN_COUNT > this.#template.length ||
|
|
@@ -1514,11 +1304,6 @@ export class MdzParser {
|
|
|
1514
1304
|
#match_heading(): boolean {
|
|
1515
1305
|
let i = this.#index;
|
|
1516
1306
|
|
|
1517
|
-
// Must start at column 0 (beginning of input or after newline)
|
|
1518
|
-
if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
|
|
1519
|
-
return false;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
1307
|
// Count hashes (must be 1-6)
|
|
1523
1308
|
let hash_count = 0;
|
|
1524
1309
|
while (
|
|
@@ -1540,22 +1325,16 @@ export class MdzParser {
|
|
|
1540
1325
|
}
|
|
1541
1326
|
i++; // consume the space
|
|
1542
1327
|
|
|
1543
|
-
// Must have non-whitespace
|
|
1544
|
-
|
|
1545
|
-
while (i < this.#template.length && this.#template.charCodeAt(i) !== NEWLINE) {
|
|
1328
|
+
// Must have at least one non-whitespace character after the space
|
|
1329
|
+
while (i < this.#template.length) {
|
|
1546
1330
|
const char_code = this.#template.charCodeAt(i);
|
|
1547
|
-
if (char_code
|
|
1548
|
-
|
|
1549
|
-
}
|
|
1331
|
+
if (char_code === NEWLINE) return false; // reached end of line with only whitespace
|
|
1332
|
+
if (char_code !== SPACE && char_code !== TAB) return true;
|
|
1550
1333
|
i++;
|
|
1551
1334
|
}
|
|
1552
1335
|
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
// At newline or EOF — both are valid
|
|
1558
|
-
return true;
|
|
1336
|
+
// Reached EOF with only whitespace after hashes
|
|
1337
|
+
return false;
|
|
1559
1338
|
}
|
|
1560
1339
|
|
|
1561
1340
|
/**
|
|
@@ -1631,11 +1410,6 @@ export class MdzParser {
|
|
|
1631
1410
|
#match_code_block(): boolean {
|
|
1632
1411
|
let i = this.#index;
|
|
1633
1412
|
|
|
1634
|
-
// Must start at column 0 (beginning of input or after newline)
|
|
1635
|
-
if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
|
|
1636
|
-
return false;
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
1413
|
// Must have at least three backticks
|
|
1640
1414
|
let backtick_count = 0;
|
|
1641
1415
|
while (i < this.#template.length && this.#template.charCodeAt(i) === BACKTICK) {
|
|
@@ -1821,3 +1595,33 @@ export class MdzParser {
|
|
|
1821
1595
|
*/
|
|
1822
1596
|
const URL_PATTERN = /^https?:\/\/[^\s)\]}<>.,:/?#!]/;
|
|
1823
1597
|
export const mdz_is_url = (s: string): boolean => URL_PATTERN.test(s);
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Resolves a relative path (`./` or `../`) against a base path.
|
|
1601
|
+
* The base is treated as a directory regardless of trailing slash
|
|
1602
|
+
* (`'/docs/mdz'` and `'/docs/mdz/'` behave identically).
|
|
1603
|
+
* Handles embedded `.` and `..` segments within the reference
|
|
1604
|
+
* (e.g., `'./a/../b'` → navigates up then down).
|
|
1605
|
+
* Clamps at root — excess `..` segments stop at `/` rather than escaping.
|
|
1606
|
+
*
|
|
1607
|
+
* @param reference A relative path starting with `./` or `../`.
|
|
1608
|
+
* @param base An absolute base path (e.g., `'/docs/mdz/'`). Empty string is treated as root.
|
|
1609
|
+
* @returns An absolute resolved path (e.g., `'/docs/mdz/grammar'`).
|
|
1610
|
+
*/
|
|
1611
|
+
export const resolve_relative_path = (reference: string, base: string): string => {
|
|
1612
|
+
const segments = base.split('/');
|
|
1613
|
+
// Remove trailing empty from split (e.g., '/docs/mdz/' → ['', 'docs', 'mdz', ''])
|
|
1614
|
+
// but keep the root segment ([''] from '' base or ['', ''] from '/').
|
|
1615
|
+
if (segments.length > 1 && segments.at(-1) === '') segments.pop();
|
|
1616
|
+
const trailing = reference.endsWith('/');
|
|
1617
|
+
for (const segment of reference.split('/')) {
|
|
1618
|
+
if (segment === '.' || segment === '') continue;
|
|
1619
|
+
if (segment === '..') {
|
|
1620
|
+
if (segments.length > 1) segments.pop(); // clamp at root
|
|
1621
|
+
} else {
|
|
1622
|
+
segments.push(segment);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (trailing) segments.push('');
|
|
1626
|
+
return segments.join('/');
|
|
1627
|
+
};
|
|
@@ -32,3 +32,12 @@ export const mdz_components_context = create_context<MdzComponents>();
|
|
|
32
32
|
* By default, no HTML elements are allowed.
|
|
33
33
|
*/
|
|
34
34
|
export const mdz_elements_context = create_context<MdzElements>();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Context for providing a base path getter for resolving relative links in mdz content.
|
|
38
|
+
* Set to a getter (e.g., `() => base`) so changes to the base prop are reflected
|
|
39
|
+
* without needing an effect. When the getter returns a path like `'/docs/mdz/'`,
|
|
40
|
+
* relative paths like `./grammar` resolve to `/docs/mdz/grammar` before rendering.
|
|
41
|
+
* When not set, relative paths use raw hrefs (browser resolves them).
|
|
42
|
+
*/
|
|
43
|
+
export const mdz_base_context = create_context<() => string | undefined>();
|