@fuzdev/fuz_ui 0.189.1 → 0.191.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/ContextmenuEntry.svelte +3 -3
- package/dist/ContextmenuLinkEntry.svelte +3 -3
- package/dist/ContextmenuRootForSafariCompatibility.svelte +1 -1
- package/dist/ContextmenuSubmenu.svelte +3 -3
- package/dist/DocsMenu.svelte +1 -1
- package/dist/DocsPageLinks.svelte +1 -1
- package/dist/DocsTertiaryNav.svelte +3 -3
- package/dist/MdzNodeView.svelte +3 -2
- package/dist/MdzNodeView.svelte.d.ts +1 -1
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/analysis_context.d.ts +2 -2
- package/dist/analysis_context.js +2 -2
- package/dist/contextmenu_state.svelte.d.ts +7 -7
- package/dist/contextmenu_state.svelte.js +7 -7
- package/dist/docs_helpers.svelte.d.ts +7 -5
- package/dist/docs_helpers.svelte.d.ts.map +1 -1
- package/dist/docs_helpers.svelte.js +7 -5
- package/dist/intersect.svelte.d.ts +1 -1
- package/dist/intersect.svelte.js +1 -1
- package/dist/library_analysis.d.ts +5 -5
- package/dist/library_analysis.js +5 -5
- package/dist/library_gen.d.ts +4 -4
- package/dist/library_gen.js +4 -4
- package/dist/library_generate.d.ts +2 -2
- package/dist/library_helpers.d.ts +8 -8
- package/dist/library_helpers.js +8 -8
- package/dist/library_pipeline.d.ts +5 -5
- package/dist/library_pipeline.js +5 -5
- package/dist/mdz.d.ts +1 -14
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +57 -156
- package/dist/mdz_helpers.d.ts +26 -2
- package/dist/mdz_helpers.d.ts.map +1 -1
- package/dist/mdz_helpers.js +59 -5
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +18 -9
- package/dist/mdz_to_svelte.d.ts +5 -5
- package/dist/mdz_to_svelte.d.ts.map +1 -1
- package/dist/mdz_to_svelte.js +5 -5
- package/dist/mdz_token_parser.js +4 -3
- package/dist/module_helpers.d.ts +8 -8
- package/dist/module_helpers.js +8 -8
- package/dist/package_helpers.d.ts +12 -12
- package/dist/package_helpers.js +12 -12
- package/dist/storage.d.ts +3 -3
- package/dist/storage.js +3 -3
- package/dist/svelte_helpers.d.ts +9 -9
- package/dist/svelte_helpers.js +9 -9
- package/dist/svelte_preprocess_mdz.d.ts +1 -1
- package/dist/svelte_preprocess_mdz.js +1 -1
- package/dist/ts_helpers.d.ts +19 -19
- package/dist/ts_helpers.js +19 -19
- package/dist/tsdoc_helpers.d.ts +5 -5
- package/dist/tsdoc_helpers.js +5 -5
- package/dist/tsdoc_mdz.js +1 -1
- package/package.json +7 -6
- package/src/lib/analysis_context.ts +2 -2
- package/src/lib/contextmenu_state.svelte.ts +7 -7
- package/src/lib/docs_helpers.svelte.ts +7 -5
- package/src/lib/intersect.svelte.ts +1 -1
- package/src/lib/library_analysis.ts +5 -5
- package/src/lib/library_gen.ts +4 -4
- package/src/lib/library_generate.ts +2 -2
- package/src/lib/library_helpers.ts +8 -8
- package/src/lib/library_pipeline.ts +5 -5
- package/src/lib/mdz.ts +63 -167
- package/src/lib/mdz_helpers.ts +60 -5
- package/src/lib/mdz_lexer.ts +20 -9
- package/src/lib/mdz_to_svelte.ts +6 -5
- package/src/lib/mdz_token_parser.ts +4 -3
- package/src/lib/module_helpers.ts +8 -8
- package/src/lib/package_helpers.ts +12 -12
- package/src/lib/storage.ts +3 -3
- package/src/lib/svelte_helpers.ts +9 -9
- package/src/lib/svelte_preprocess_mdz.ts +1 -1
- package/src/lib/ts_helpers.ts +19 -19
- package/src/lib/tsdoc_helpers.ts +5 -5
- package/src/lib/tsdoc_mdz.ts +1 -1
|
@@ -344,7 +344,7 @@ let cache_key_counter = 0;
|
|
|
344
344
|
|
|
345
345
|
/**
|
|
346
346
|
* Creates an attachment that sets up contextmenu behavior on an element.
|
|
347
|
-
* @param params
|
|
347
|
+
* @param params - contextmenu parameters or nullish to disable
|
|
348
348
|
*/
|
|
349
349
|
export const contextmenu_attachment =
|
|
350
350
|
<T extends ContextmenuParams, U extends T | Array<T>>(
|
|
@@ -380,11 +380,11 @@ export interface ContextmenuOpenOptions {
|
|
|
380
380
|
/**
|
|
381
381
|
* Opens the contextmenu, if appropriate,
|
|
382
382
|
* querying the menu items from the DOM starting at the event target.
|
|
383
|
-
* @param target the leaf element from which to open the contextmenu
|
|
384
|
-
* @param x the page X coordinate at which to open the contextmenu, typically the mouse `pageX`
|
|
385
|
-
* @param y the page Y coordinate at which to open the contextmenu, typically the mouse `pageY`
|
|
386
|
-
* @param contextmenu the contextmenu store
|
|
387
|
-
* @param options optional configuration for filtering entries and haptic feedback
|
|
383
|
+
* @param target - the leaf element from which to open the contextmenu
|
|
384
|
+
* @param x - the page X coordinate at which to open the contextmenu, typically the mouse `pageX`
|
|
385
|
+
* @param y - the page Y coordinate at which to open the contextmenu, typically the mouse `pageY`
|
|
386
|
+
* @param contextmenu - the contextmenu store
|
|
387
|
+
* @param options - optional configuration for filtering entries and haptic feedback
|
|
388
388
|
* @returns a boolean indicating if the menu was opened or not
|
|
389
389
|
*/
|
|
390
390
|
export const contextmenu_open = (
|
|
@@ -487,7 +487,7 @@ const non_scoped_roots: Set<symbol> = new Set();
|
|
|
487
487
|
* Registers a contextmenu root and warns if multiple non-scoped roots are detected.
|
|
488
488
|
* Only active in development mode. Automatically handles cleanup on unmount.
|
|
489
489
|
*
|
|
490
|
-
* @param get_scoped
|
|
490
|
+
* @param get_scoped - getter function that returns the current scoped value
|
|
491
491
|
*/
|
|
492
492
|
export const contextmenu_check_global_root = (get_scoped: () => boolean): void => {
|
|
493
493
|
$effect(() => {
|
|
@@ -5,11 +5,13 @@ import {ensure_end, ensure_start} from '@fuzdev/fuz_util/string.js';
|
|
|
5
5
|
import {create_context} from './context_helpers.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Convert a string to a URL-safe fragment identifier, preserving case
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Convert a string to a URL-safe fragment identifier, preserving case.
|
|
9
|
+
* Unlike `slugify` from `@fuzdev/fuz_util/path.js` which lowercases,
|
|
10
|
+
* this keeps the original casing so API declarations like `AsyncStatus`
|
|
11
|
+
* and `async_status` produce distinct fragment IDs.
|
|
12
|
+
* Used by the Tome documentation system for heading and section anchors.
|
|
13
|
+
* @param str - the string to convert to a fragment
|
|
14
|
+
* @returns a URL-safe fragment identifier with case preserved
|
|
13
15
|
*/
|
|
14
16
|
export const docs_slugify = (str: string): string => {
|
|
15
17
|
return (
|
|
@@ -30,7 +30,7 @@ export type IntersectParamsOrCallback = OnIntersect | IntersectParams;
|
|
|
30
30
|
* Creates an attachment that observes element viewport intersection.
|
|
31
31
|
* Uses the lazy function pattern to optimize reactivity:
|
|
32
32
|
* callbacks can update without recreating the observer, preserving state.
|
|
33
|
-
* @param get_params
|
|
33
|
+
* @param get_params - function that returns callback, params object, or nullish to disable
|
|
34
34
|
*/
|
|
35
35
|
export const intersect =
|
|
36
36
|
(
|
|
@@ -113,11 +113,11 @@ export interface ModuleAnalysis {
|
|
|
113
113
|
* only re-analyze changed files. The TypeScript program should include all files
|
|
114
114
|
* for accurate type resolution, but only changed files need re-analysis.
|
|
115
115
|
*
|
|
116
|
-
* @param source_file
|
|
117
|
-
* @param program TypeScript program (used for type checking and source file lookup)
|
|
118
|
-
* @param options
|
|
119
|
-
* @param ctx
|
|
120
|
-
* @param log
|
|
116
|
+
* @param source_file - the source file info with content and optional dependency data
|
|
117
|
+
* @param program - TypeScript program (used for type checking and source file lookup)
|
|
118
|
+
* @param options - module source options for path extraction
|
|
119
|
+
* @param ctx - analysis context for collecting diagnostics
|
|
120
|
+
* @param log - optional logger for warnings
|
|
121
121
|
* @returns Module metadata and re-exports, or undefined if source file not found in program
|
|
122
122
|
*/
|
|
123
123
|
export const library_analyze_module = (
|
package/src/lib/library_gen.ts
CHANGED
|
@@ -89,9 +89,9 @@ export const source_file_from_disknode = (disknode: Disknode): SourceFileInfo =>
|
|
|
89
89
|
* have malformed paths or missing content). The filtering uses `module_is_source` which
|
|
90
90
|
* checks `source_paths` to only include files in configured source directories.
|
|
91
91
|
*
|
|
92
|
-
* @param disknodes
|
|
93
|
-
* @param options
|
|
94
|
-
* @param log
|
|
92
|
+
* @param disknodes - iterator of Gro disknodes from filer
|
|
93
|
+
* @param options - module source options for filtering
|
|
94
|
+
* @param log - optional logger for status messages
|
|
95
95
|
*/
|
|
96
96
|
export const library_collect_source_files_from_disknodes = (
|
|
97
97
|
disknodes: Iterable<Disknode>,
|
|
@@ -146,7 +146,7 @@ export const library_collect_source_files_from_disknodes = (
|
|
|
146
146
|
* export const gen = library_gen();
|
|
147
147
|
* ```
|
|
148
148
|
*
|
|
149
|
-
* @param options
|
|
149
|
+
* @param options - optional generation options
|
|
150
150
|
*/
|
|
151
151
|
export const library_gen = (options?: LibraryGenOptions): Gen => {
|
|
152
152
|
return {
|
|
@@ -43,8 +43,8 @@ import {AnalysisContext, format_diagnostic} from './analysis_context.js';
|
|
|
43
43
|
/**
|
|
44
44
|
* Callback for handling duplicate declaration names.
|
|
45
45
|
*
|
|
46
|
-
* @param duplicates
|
|
47
|
-
* @param log
|
|
46
|
+
* @param duplicates - map of declaration names to their occurrences across modules
|
|
47
|
+
* @param log - logger for reporting
|
|
48
48
|
*/
|
|
49
49
|
export type OnDuplicatesCallback = (
|
|
50
50
|
duplicates: Map<string, Array<DuplicateInfo>>,
|
|
@@ -17,7 +17,7 @@ import {DOCS_API_PATH, DOCS_PATH_DEFAULT} from './docs_helpers.svelte.js';
|
|
|
17
17
|
/**
|
|
18
18
|
* Build project-relative API documentation URL with hash anchor.
|
|
19
19
|
*
|
|
20
|
-
* @param declaration_name
|
|
20
|
+
* @param declaration_name - name of the declaration to link to
|
|
21
21
|
* @returns URL path like '/docs/api#declaration_name'
|
|
22
22
|
*/
|
|
23
23
|
export const url_api_declaration = (declaration_name: string): string =>
|
|
@@ -26,8 +26,8 @@ export const url_api_declaration = (declaration_name: string): string =>
|
|
|
26
26
|
/**
|
|
27
27
|
* Build full API documentation URL with domain and hash anchor.
|
|
28
28
|
*
|
|
29
|
-
* @param homepage
|
|
30
|
-
* @param declaration_name
|
|
29
|
+
* @param homepage - package homepage URL
|
|
30
|
+
* @param declaration_name - name of the declaration to link to
|
|
31
31
|
* @returns Full URL like 'https://example.com/docs/api#declaration_name'
|
|
32
32
|
*/
|
|
33
33
|
export const url_api_declaration_full = (homepage: string, declaration_name: string): string =>
|
|
@@ -36,7 +36,7 @@ export const url_api_declaration_full = (homepage: string, declaration_name: str
|
|
|
36
36
|
/**
|
|
37
37
|
* Build project-relative module documentation URL.
|
|
38
38
|
*
|
|
39
|
-
* @param module_path
|
|
39
|
+
* @param module_path - module path (e.g., 'helpers.ts')
|
|
40
40
|
* @returns URL path like '/docs/api/helpers.ts'
|
|
41
41
|
*/
|
|
42
42
|
export const url_api_module = (module_path: string): string => `${DOCS_API_PATH}/${module_path}`;
|
|
@@ -44,8 +44,8 @@ export const url_api_module = (module_path: string): string => `${DOCS_API_PATH}
|
|
|
44
44
|
/**
|
|
45
45
|
* Build package logo URL with favicon.png fallback.
|
|
46
46
|
*
|
|
47
|
-
* @param homepage_url
|
|
48
|
-
* @param logo_path
|
|
47
|
+
* @param homepage_url - package homepage URL, or null
|
|
48
|
+
* @param logo_path - optional custom logo path (defaults to 'favicon.png')
|
|
49
49
|
* @returns Full URL to the logo, or null if no homepage
|
|
50
50
|
*/
|
|
51
51
|
export const url_package_logo = (
|
|
@@ -62,8 +62,8 @@ export const url_package_logo = (
|
|
|
62
62
|
*
|
|
63
63
|
* Uses SvelteKit's page state for the current origin by default.
|
|
64
64
|
*
|
|
65
|
-
* @param url
|
|
66
|
-
* @param origin
|
|
65
|
+
* @param url - full URL to convert
|
|
66
|
+
* @param origin - origin to strip (defaults to current page origin)
|
|
67
67
|
* @returns Root-relative URL starting with '/'
|
|
68
68
|
*
|
|
69
69
|
* @example
|
|
@@ -130,8 +130,8 @@ export interface CollectedReExport {
|
|
|
130
130
|
* // - helpers.ts foo declaration gets: also_exported_from: ['index.ts']
|
|
131
131
|
* // - helpers.ts bar declaration gets: also_exported_from: ['index.ts']
|
|
132
132
|
*
|
|
133
|
-
* @param source_json
|
|
134
|
-
* @param collected_re_exports
|
|
133
|
+
* @param source_json - the source JSON with all modules (will be mutated)
|
|
134
|
+
* @param collected_re_exports - array of re-exports collected during phase 1
|
|
135
135
|
* @mutates source_json - adds `also_exported_from` to declarations
|
|
136
136
|
*/
|
|
137
137
|
export const library_merge_re_exports = (
|
|
@@ -180,9 +180,9 @@ export const library_merge_re_exports = (
|
|
|
180
180
|
* File types are determined by `options.get_analyzer`. By default, `.ts`, `.js`, and `.svelte`
|
|
181
181
|
* files are supported. Customize `get_analyzer` to support additional file types like `.svx`.
|
|
182
182
|
*
|
|
183
|
-
* @param files
|
|
184
|
-
* @param options
|
|
185
|
-
* @param log
|
|
183
|
+
* @param files - iterable of source file info (from Gro filer, file system, or other source)
|
|
184
|
+
* @param options - module source options for filtering
|
|
185
|
+
* @param log - optional logger for status messages
|
|
186
186
|
*/
|
|
187
187
|
export const library_collect_source_files = (
|
|
188
188
|
files: Iterable<SourceFileInfo>,
|
package/src/lib/mdz.ts
CHANGED
|
@@ -62,6 +62,8 @@ import {
|
|
|
62
62
|
is_at_absolute_path,
|
|
63
63
|
is_at_relative_path,
|
|
64
64
|
extract_single_tag,
|
|
65
|
+
mdz_heading_id,
|
|
66
|
+
mdz_is_url,
|
|
65
67
|
} from './mdz_helpers.js';
|
|
66
68
|
|
|
67
69
|
// TODO design incremental parsing or some system that preserves Svelte components across re-renders when possible
|
|
@@ -141,6 +143,7 @@ export interface MdzHrNode extends MdzBaseNode {
|
|
|
141
143
|
export interface MdzHeadingNode extends MdzBaseNode {
|
|
142
144
|
type: 'Heading';
|
|
143
145
|
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
146
|
+
id: string; // slugified heading text for fragment links
|
|
144
147
|
children: Array<MdzNode>; // inline formatting allowed
|
|
145
148
|
}
|
|
146
149
|
|
|
@@ -718,107 +721,41 @@ export class MdzParser {
|
|
|
718
721
|
#parse_tag(): MdzElementNode | MdzComponentNode | MdzTextNode {
|
|
719
722
|
const start = this.#index;
|
|
720
723
|
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
this.#accumulated_text = '';
|
|
726
|
-
this.#nodes.length = 0;
|
|
727
|
-
|
|
728
|
-
// Consume <
|
|
729
|
-
if (!this.#match('<')) {
|
|
730
|
-
// Restore parent state before returning
|
|
731
|
-
this.#restore_accumulation_state(saved_state);
|
|
732
|
-
|
|
733
|
-
const content = this.#template[this.#index]!;
|
|
734
|
-
this.#index++;
|
|
735
|
-
return {
|
|
736
|
-
type: 'Text',
|
|
737
|
-
content,
|
|
738
|
-
start,
|
|
739
|
-
end: this.#index,
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
this.#index++;
|
|
743
|
-
|
|
744
|
-
// Parse tag name
|
|
745
|
-
const tag_name_start = this.#index;
|
|
746
|
-
let tag_name_end = this.#index;
|
|
724
|
+
// Phase 1: Validate tag structure using a local scan index.
|
|
725
|
+
// Avoids save/restore of accumulation state on the fast path
|
|
726
|
+
// (most `<` characters are not valid tags).
|
|
727
|
+
let i = start + 1; // skip <
|
|
747
728
|
|
|
748
729
|
// Tag name must start with a letter
|
|
749
|
-
if (this.#
|
|
750
|
-
|
|
751
|
-
this.#
|
|
752
|
-
return {
|
|
753
|
-
type: 'Text',
|
|
754
|
-
content: '<',
|
|
755
|
-
start,
|
|
756
|
-
end: this.#index,
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const first_char = this.#template.charCodeAt(this.#index);
|
|
761
|
-
if (!is_letter(first_char)) {
|
|
762
|
-
// Not a valid tag, treat as text - restore parent state
|
|
763
|
-
this.#restore_accumulation_state(saved_state);
|
|
764
|
-
return {
|
|
765
|
-
type: 'Text',
|
|
766
|
-
content: '<',
|
|
767
|
-
start,
|
|
768
|
-
end: start + 1,
|
|
769
|
-
};
|
|
730
|
+
if (i >= this.#template.length || !is_letter(this.#template.charCodeAt(i))) {
|
|
731
|
+
this.#index = start + 1;
|
|
732
|
+
return this.#make_text_node('<', start);
|
|
770
733
|
}
|
|
771
734
|
|
|
772
735
|
// Collect tag name (letters, numbers, hyphens, underscores)
|
|
773
|
-
while (this.#
|
|
774
|
-
|
|
775
|
-
if (is_tag_name_char(char_code)) {
|
|
776
|
-
this.#index++;
|
|
777
|
-
} else {
|
|
778
|
-
break;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
tag_name_end = this.#index;
|
|
783
|
-
const tag_name = this.#template.slice(tag_name_start, tag_name_end);
|
|
784
|
-
|
|
785
|
-
if (tag_name.length === 0) {
|
|
786
|
-
// Empty tag name - restore parent state
|
|
787
|
-
this.#restore_accumulation_state(saved_state);
|
|
788
|
-
return {
|
|
789
|
-
type: 'Text',
|
|
790
|
-
content: '<',
|
|
791
|
-
start,
|
|
792
|
-
end: start + 1,
|
|
793
|
-
};
|
|
736
|
+
while (i < this.#template.length && is_tag_name_char(this.#template.charCodeAt(i))) {
|
|
737
|
+
i++;
|
|
794
738
|
}
|
|
795
739
|
|
|
796
|
-
|
|
740
|
+
const tag_name = this.#template.slice(start + 1, i);
|
|
797
741
|
const first_char_code = tag_name.charCodeAt(0);
|
|
798
|
-
const
|
|
799
|
-
|
|
742
|
+
const node_type: 'Component' | 'Element' =
|
|
743
|
+
first_char_code >= A_UPPER && first_char_code <= Z_UPPER ? 'Component' : 'Element';
|
|
800
744
|
|
|
801
745
|
// Skip whitespace after tag name (for future attribute support)
|
|
802
|
-
while (
|
|
803
|
-
|
|
804
|
-
this.#template.charCodeAt(this.#index) === SPACE
|
|
805
|
-
) {
|
|
806
|
-
this.#index++;
|
|
746
|
+
while (i < this.#template.length && this.#template.charCodeAt(i) === SPACE) {
|
|
747
|
+
i++;
|
|
807
748
|
}
|
|
808
749
|
|
|
809
750
|
// TODO: Parse attributes here
|
|
810
751
|
|
|
811
752
|
// Check for self-closing />
|
|
812
753
|
if (
|
|
813
|
-
|
|
814
|
-
this.#template.charCodeAt(
|
|
815
|
-
this.#template.charCodeAt(
|
|
754
|
+
i + 1 < this.#template.length &&
|
|
755
|
+
this.#template.charCodeAt(i) === SLASH &&
|
|
756
|
+
this.#template.charCodeAt(i + 1) === RIGHT_ANGLE
|
|
816
757
|
) {
|
|
817
|
-
this.#index
|
|
818
|
-
|
|
819
|
-
// Restore parent state before returning
|
|
820
|
-
this.#restore_accumulation_state(saved_state);
|
|
821
|
-
|
|
758
|
+
this.#index = i + 2;
|
|
822
759
|
return {
|
|
823
760
|
type: node_type,
|
|
824
761
|
name: tag_name,
|
|
@@ -828,41 +765,44 @@ export class MdzParser {
|
|
|
828
765
|
};
|
|
829
766
|
}
|
|
830
767
|
|
|
831
|
-
//
|
|
832
|
-
if (
|
|
833
|
-
this.#index >= this.#template.length ||
|
|
834
|
-
this.#template.charCodeAt(this.#index) !== RIGHT_ANGLE
|
|
835
|
-
) {
|
|
836
|
-
// Unclosed opening tag, treat as text - restore parent state
|
|
837
|
-
this.#restore_accumulation_state(saved_state);
|
|
838
|
-
|
|
768
|
+
// Must have closing >
|
|
769
|
+
if (i >= this.#template.length || this.#template.charCodeAt(i) !== RIGHT_ANGLE) {
|
|
839
770
|
this.#index = start + 1;
|
|
840
|
-
return
|
|
841
|
-
type: 'Text',
|
|
842
|
-
content: '<',
|
|
843
|
-
start,
|
|
844
|
-
end: this.#index,
|
|
845
|
-
};
|
|
771
|
+
return this.#make_text_node('<', start);
|
|
846
772
|
}
|
|
847
773
|
|
|
848
|
-
//
|
|
849
|
-
this.#index++;
|
|
774
|
+
const content_start = i + 1; // past >
|
|
850
775
|
|
|
851
|
-
//
|
|
776
|
+
// Bail early if closing tag is missing, past a paragraph break,
|
|
777
|
+
// or past the search boundary (e.g. heading line end)
|
|
852
778
|
const closing_tag = `</${tag_name}>`;
|
|
779
|
+
const search_limit = Math.min(this.#max_search_index, this.#template.length);
|
|
780
|
+
const close_tag_index = this.#template.indexOf(closing_tag, content_start);
|
|
781
|
+
if (close_tag_index === -1 || close_tag_index >= search_limit) {
|
|
782
|
+
this.#index = start + 1;
|
|
783
|
+
return this.#make_text_node('<', start);
|
|
784
|
+
}
|
|
785
|
+
const para_break = this.#template.indexOf('\n\n', content_start);
|
|
786
|
+
if (para_break !== -1 && para_break < close_tag_index) {
|
|
787
|
+
this.#index = start + 1;
|
|
788
|
+
return this.#make_text_node('<', start);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Phase 2: Tag validated — save accumulation state and parse children.
|
|
792
|
+
const saved_state = this.#save_accumulation_state();
|
|
793
|
+
this.#accumulated_text = '';
|
|
794
|
+
this.#nodes.length = 0;
|
|
795
|
+
this.#index = content_start;
|
|
796
|
+
|
|
853
797
|
const children: Array<MdzNode> = [];
|
|
854
798
|
|
|
855
799
|
while (this.#index < this.#template.length) {
|
|
856
|
-
// Check for closing tag
|
|
857
800
|
if (this.#match(closing_tag)) {
|
|
858
|
-
// Flush any accumulated text from children
|
|
859
801
|
this.#flush_text();
|
|
860
802
|
children.push(...this.#nodes);
|
|
861
803
|
this.#nodes.length = 0;
|
|
862
804
|
|
|
863
805
|
this.#index += closing_tag.length;
|
|
864
|
-
|
|
865
|
-
// Restore parent state before returning
|
|
866
806
|
this.#restore_accumulation_state(saved_state);
|
|
867
807
|
|
|
868
808
|
return {
|
|
@@ -885,17 +825,10 @@ export class MdzParser {
|
|
|
885
825
|
}
|
|
886
826
|
}
|
|
887
827
|
|
|
888
|
-
//
|
|
889
|
-
// Treat the opening tag as text - restore parent state
|
|
828
|
+
// Defensive: pre-check guarantees closing tag exists, but handle EOF gracefully
|
|
890
829
|
this.#restore_accumulation_state(saved_state);
|
|
891
|
-
|
|
892
830
|
this.#index = start + 1;
|
|
893
|
-
return
|
|
894
|
-
type: 'Text',
|
|
895
|
-
content: '<',
|
|
896
|
-
start,
|
|
897
|
-
end: this.#index,
|
|
898
|
-
};
|
|
831
|
+
return this.#make_text_node('<', start);
|
|
899
832
|
}
|
|
900
833
|
|
|
901
834
|
/**
|
|
@@ -1354,31 +1287,29 @@ export class MdzParser {
|
|
|
1354
1287
|
// Consume the space after hashes (already verified to exist)
|
|
1355
1288
|
this.#index++;
|
|
1356
1289
|
|
|
1357
|
-
//
|
|
1358
|
-
|
|
1290
|
+
// Find end-of-line to bound nested parsers (prevents tag scanner from scanning past heading)
|
|
1291
|
+
let eol = this.#template.indexOf('\n', this.#index);
|
|
1292
|
+
if (eol === -1) eol = this.#template.length;
|
|
1359
1293
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1294
|
+
const saved_max_search_index = this.#max_search_index;
|
|
1295
|
+
this.#max_search_index = eol;
|
|
1362
1296
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
}
|
|
1297
|
+
// Parse inline content until end of line
|
|
1298
|
+
const content_nodes: Array<MdzNode> = [];
|
|
1366
1299
|
|
|
1300
|
+
while (this.#index < eol) {
|
|
1367
1301
|
const node = this.#parse_node();
|
|
1368
1302
|
if (node.type === 'Text') {
|
|
1369
|
-
//
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
if (newline_index !== -1) {
|
|
1373
|
-
const trimmed_content = node.content.slice(0, newline_index);
|
|
1303
|
+
// Trim if #parse_text overshot past the newline
|
|
1304
|
+
if (node.end > eol) {
|
|
1305
|
+
const trimmed_content = node.content.slice(0, eol - node.start);
|
|
1374
1306
|
if (trimmed_content) {
|
|
1375
1307
|
this.#accumulate_text(trimmed_content, node.start);
|
|
1376
1308
|
}
|
|
1377
|
-
this.#index =
|
|
1309
|
+
this.#index = eol;
|
|
1378
1310
|
break;
|
|
1379
|
-
} else {
|
|
1380
|
-
this.#accumulate_text(node.content, node.start);
|
|
1381
1311
|
}
|
|
1312
|
+
this.#accumulate_text(node.content, node.start);
|
|
1382
1313
|
} else {
|
|
1383
1314
|
this.#flush_text();
|
|
1384
1315
|
content_nodes.push(...this.#nodes);
|
|
@@ -1387,6 +1318,8 @@ export class MdzParser {
|
|
|
1387
1318
|
}
|
|
1388
1319
|
}
|
|
1389
1320
|
|
|
1321
|
+
this.#max_search_index = saved_max_search_index;
|
|
1322
|
+
|
|
1390
1323
|
this.#flush_text();
|
|
1391
1324
|
content_nodes.push(...this.#nodes);
|
|
1392
1325
|
this.#nodes.length = 0;
|
|
@@ -1396,6 +1329,7 @@ export class MdzParser {
|
|
|
1396
1329
|
return {
|
|
1397
1330
|
type: 'Heading',
|
|
1398
1331
|
level: level as 1 | 2 | 3 | 4 | 5 | 6,
|
|
1332
|
+
id: mdz_heading_id(content_nodes),
|
|
1399
1333
|
children: content_nodes,
|
|
1400
1334
|
start,
|
|
1401
1335
|
end: this.#index,
|
|
@@ -1587,41 +1521,3 @@ export class MdzParser {
|
|
|
1587
1521
|
throw Error('Code block not properly closed');
|
|
1588
1522
|
}
|
|
1589
1523
|
}
|
|
1590
|
-
|
|
1591
|
-
/**
|
|
1592
|
-
* Check if a string is a URL (`https://` or `http://`).
|
|
1593
|
-
* Requires at least one valid character after the protocol.
|
|
1594
|
-
* Rejects whitespace and characters that can't start a valid hostname.
|
|
1595
|
-
*/
|
|
1596
|
-
const URL_PATTERN = /^https?:\/\/[^\s)\]}<>.,:/?#!]/;
|
|
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
|
-
};
|
package/src/lib/mdz_helpers.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type {MdzNode, MdzComponentNode, MdzElementNode} from './mdz.js';
|
|
11
|
+
import {slugify} from '@fuzdev/fuz_util/path.js';
|
|
11
12
|
|
|
12
13
|
// Character codes for performance
|
|
13
14
|
export const BACKTICK = 96; // `
|
|
@@ -187,7 +188,7 @@ export const trim_trailing_punctuation = (url: string): string => {
|
|
|
187
188
|
/**
|
|
188
189
|
* Check if position in text is the start of an absolute path (starts with `/`).
|
|
189
190
|
* Must be preceded by whitespace or be at the start of the string.
|
|
190
|
-
* Rejects `//` (comments/protocol-relative) and
|
|
191
|
+
* Rejects `//` (comments/protocol-relative) and slash followed by whitespace.
|
|
191
192
|
*/
|
|
192
193
|
export const is_at_absolute_path = (text: string, index: number): boolean => {
|
|
193
194
|
if (text.charCodeAt(index) !== SLASH) return false;
|
|
@@ -197,13 +198,13 @@ export const is_at_absolute_path = (text: string, index: number): boolean => {
|
|
|
197
198
|
}
|
|
198
199
|
if (index + 1 >= text.length) return false;
|
|
199
200
|
const next_char = text.charCodeAt(index + 1);
|
|
200
|
-
return next_char !== SLASH && next_char !== SPACE && next_char !== NEWLINE;
|
|
201
|
+
return next_char !== SLASH && next_char !== SPACE && next_char !== NEWLINE && next_char !== TAB;
|
|
201
202
|
};
|
|
202
203
|
|
|
203
204
|
/**
|
|
204
205
|
* Check if position in text is the start of a relative path (`./` or `../`).
|
|
205
206
|
* Must be preceded by whitespace or be at the start of the string.
|
|
206
|
-
*
|
|
207
|
+
* Rejects prefix followed by whitespace, slash, or end of string.
|
|
207
208
|
*/
|
|
208
209
|
export const is_at_relative_path = (text: string, index: number): boolean => {
|
|
209
210
|
if (text.charCodeAt(index) !== PERIOD) return false;
|
|
@@ -219,16 +220,70 @@ export const is_at_relative_path = (text: string, index: number): boolean => {
|
|
|
219
220
|
text.charCodeAt(index + 2) === SLASH
|
|
220
221
|
) {
|
|
221
222
|
const after = text.charCodeAt(index + 3);
|
|
222
|
-
return after !== SPACE && after !== NEWLINE && after !== SLASH;
|
|
223
|
+
return after !== SPACE && after !== NEWLINE && after !== TAB && after !== SLASH;
|
|
223
224
|
}
|
|
224
225
|
// Check for ./ (at least 3 chars: ./x)
|
|
225
226
|
if (remaining >= 3 && text.charCodeAt(index + 1) === SLASH) {
|
|
226
227
|
const after = text.charCodeAt(index + 2);
|
|
227
|
-
return after !== SPACE && after !== NEWLINE && after !== SLASH;
|
|
228
|
+
return after !== SPACE && after !== NEWLINE && after !== TAB && after !== SLASH;
|
|
228
229
|
}
|
|
229
230
|
return false;
|
|
230
231
|
};
|
|
231
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Extracts plain text content from an array of mdz nodes, recursing into children.
|
|
235
|
+
*/
|
|
236
|
+
export const mdz_text_content = (nodes: Array<MdzNode>): string =>
|
|
237
|
+
nodes
|
|
238
|
+
.map((n) => ('children' in n ? mdz_text_content(n.children) : 'content' in n ? n.content : ''))
|
|
239
|
+
.join('');
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generates a lowercase slug id for a heading from its child nodes.
|
|
243
|
+
* Follows standard markdown conventions (GitHub, etc.) where heading IDs are lowercased.
|
|
244
|
+
* For case-preserving IDs (e.g. API declarations), see `docs_slugify` in `docs_helpers.svelte.ts`.
|
|
245
|
+
*/
|
|
246
|
+
export const mdz_heading_id = (nodes: Array<MdzNode>): string =>
|
|
247
|
+
slugify(mdz_text_content(nodes), false);
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if a string is a URL (`https://` or `http://`).
|
|
251
|
+
* Requires at least one valid character after the protocol.
|
|
252
|
+
* Rejects whitespace and characters that can't start a valid hostname.
|
|
253
|
+
*/
|
|
254
|
+
const URL_PATTERN = /^https?:\/\/[^\s)\]}<>.,:/?#!]/;
|
|
255
|
+
export const mdz_is_url = (s: string): boolean => URL_PATTERN.test(s);
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Resolves a relative path (`./` or `../`) against a base path.
|
|
259
|
+
* The base is treated as a directory regardless of trailing slash
|
|
260
|
+
* (`'/docs/mdz'` and `'/docs/mdz/'` behave identically).
|
|
261
|
+
* Handles embedded `.` and `..` segments within the reference
|
|
262
|
+
* (e.g., `'./a/../b'` → navigates up then down).
|
|
263
|
+
* Clamps at root — excess `..` segments stop at `/` rather than escaping.
|
|
264
|
+
*
|
|
265
|
+
* @param reference - a relative path starting with `./` or `../`.
|
|
266
|
+
* @param base - an absolute base path (e.g., `'/docs/mdz/'`). Empty string is treated as root.
|
|
267
|
+
* @returns An absolute resolved path (e.g., `'/docs/mdz/grammar'`).
|
|
268
|
+
*/
|
|
269
|
+
export const resolve_relative_path = (reference: string, base: string): string => {
|
|
270
|
+
const segments = base.split('/');
|
|
271
|
+
// Remove trailing empty from split (e.g., '/docs/mdz/' → ['', 'docs', 'mdz', ''])
|
|
272
|
+
// but keep the root segment ([''] from '' base or ['', ''] from '/').
|
|
273
|
+
if (segments.length > 1 && segments.at(-1) === '') segments.pop();
|
|
274
|
+
const trailing = reference.endsWith('/');
|
|
275
|
+
for (const segment of reference.split('/')) {
|
|
276
|
+
if (segment === '.' || segment === '') continue;
|
|
277
|
+
if (segment === '..') {
|
|
278
|
+
if (segments.length > 1) segments.pop(); // clamp at root
|
|
279
|
+
} else {
|
|
280
|
+
segments.push(segment);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (trailing) segments.push('');
|
|
284
|
+
return segments.join('/');
|
|
285
|
+
};
|
|
286
|
+
|
|
232
287
|
export const extract_single_tag = (
|
|
233
288
|
nodes: Array<MdzNode>,
|
|
234
289
|
): MdzComponentNode | MdzElementNode | null => {
|