@fuzdev/fuz_ui 0.185.1 → 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/autofocus.svelte.d.ts +14 -0
- package/dist/autofocus.svelte.d.ts.map +1 -0
- package/dist/autofocus.svelte.js +15 -0
- 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/autofocus.svelte.ts +20 -0
- 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/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
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
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 =
|
|
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.#
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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
|
-
*
|
|
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 (
|
|
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 (
|
|
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.#
|
|
935
|
-
if (char_code === SPACE || char_code === NEWLINE || !
|
|
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 =
|
|
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
|
|
783
|
+
* Parse auto-detected path (absolute `/`, relative `./` or `../`).
|
|
956
784
|
* Uses RFC 3986 whitelist validation for valid URI characters.
|
|
957
785
|
*/
|
|
958
|
-
#
|
|
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.#
|
|
964
|
-
if (char_code === SPACE || char_code === NEWLINE || !
|
|
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 =
|
|
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.#
|
|
1046
|
-
|
|
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.#
|
|
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 (
|
|
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.#
|
|
1150
|
-
this.#index
|
|
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
|
|
1250
|
-
|
|
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
|
|
1254
|
-
|
|
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
|
-
|
|
1259
|
-
|
|
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
|
+
};
|
package/dist/mdz_components.d.ts
CHANGED
|
@@ -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"}
|
package/dist/mdz_components.js
CHANGED
|
@@ -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();
|