@vmz/core 0.1.24 → 0.1.26
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/dom-core.d.ts +43 -4
- package/dist/dom-core.js +148 -22
- package/dist/dom-ssr.d.ts +3 -1
- package/dist/dom-ssr.js +210 -78
- package/dist/localize-body-links.d.ts +24 -5
- package/dist/localize-body-links.js +55 -58
- package/package.json +7 -7
package/dist/dom-core.d.ts
CHANGED
|
@@ -71,6 +71,14 @@ export declare function settlePendingChildMounts(inst: any): Promise<void>;
|
|
|
71
71
|
*/
|
|
72
72
|
export declare function runDirectCreate(Component: any, inst: any): any;
|
|
73
73
|
/** Host API for compiler-emitted `__vmzCreate` (direct path, Program IR B). */
|
|
74
|
+
/**
|
|
75
|
+
* First default `<slot>` owned by this component tree — skip nested `[data-vmz]`
|
|
76
|
+
* hosts (Button/Link/Empty label slots). `querySelector('slot')` steals those and
|
|
77
|
+
* projects parent siblings into the wrong host (commercial Drawer into Create btn).
|
|
78
|
+
* @param {Element | null | undefined} root
|
|
79
|
+
* @returns {Element | null}
|
|
80
|
+
*/
|
|
81
|
+
export declare function findOwnedDefaultSlot(root: any): any;
|
|
74
82
|
export declare const directApi: {
|
|
75
83
|
/** @type {object | null} */
|
|
76
84
|
_inst: any;
|
|
@@ -86,8 +94,22 @@ export declare const directApi: {
|
|
|
86
94
|
* }}
|
|
87
95
|
*/
|
|
88
96
|
_eachCtx: any;
|
|
97
|
+
/** @type {null | {
|
|
98
|
+
* el: (tag: string) => Element,
|
|
99
|
+
* text: (value?: any) => Text,
|
|
100
|
+
* componentHost?: (name: string) => HTMLElement | null,
|
|
101
|
+
* enter?: (node: Element) => boolean,
|
|
102
|
+
* leave?: () => void,
|
|
103
|
+
* scopeDepth?: () => number,
|
|
104
|
+
* rewindScope?: (depth: number) => void,
|
|
105
|
+
* beginBranchScope?: () => () => void,
|
|
106
|
+
* }} */
|
|
107
|
+
_resumeAdopt: any;
|
|
89
108
|
el(tag: any): any;
|
|
90
|
-
text(value: any):
|
|
109
|
+
text(value: any): any;
|
|
110
|
+
/** Push adopted node's private resume pool for depth-first child create. */
|
|
111
|
+
adoptEnter(node: any): any;
|
|
112
|
+
adoptLeave(): void;
|
|
91
113
|
frag(): DocumentFragment;
|
|
92
114
|
attr(el: any, name: any, value: any): void;
|
|
93
115
|
on(el: any, type: any, handler: any): void;
|
|
@@ -138,7 +160,15 @@ export declare const directApi: {
|
|
|
138
160
|
* @param {Record<string, any>} props
|
|
139
161
|
* @param {string | null} client
|
|
140
162
|
*/
|
|
141
|
-
component(hostInst: any, name: any, props: any, client: any):
|
|
163
|
+
component(hostInst: any, name: any, props: any, client: any): any;
|
|
164
|
+
/**
|
|
165
|
+
* Subscribe to a child component event (`@submit` → event name `submit`).
|
|
166
|
+
* Orthogonal to function props (`:on-submit` → prop `onSubmit`).
|
|
167
|
+
* @param {HTMLElement} hostEl
|
|
168
|
+
* @param {string} eventName
|
|
169
|
+
* @param {(ev?: any, ...rest: any[]) => any} handler
|
|
170
|
+
*/
|
|
171
|
+
onComponentEvent(hostEl: any, eventName: any, handler: any): void;
|
|
142
172
|
/**
|
|
143
173
|
* Keep nested Direct child props live with parent field writes.
|
|
144
174
|
* @param {object} hostInst
|
|
@@ -150,6 +180,7 @@ export declare const directApi: {
|
|
|
150
180
|
bindComponentProp(hostInst: any, hostEl: any, propName: any, deps: any, get: any): void;
|
|
151
181
|
/**
|
|
152
182
|
* Project parent children into nested Direct component default `<slot>`.
|
|
183
|
+
* Uses {@link findOwnedDefaultSlot} so nested Button/Link slots are not stolen.
|
|
153
184
|
* @param {HTMLElement} hostEl
|
|
154
185
|
* @param {Node} node
|
|
155
186
|
*/
|
|
@@ -175,12 +206,20 @@ export declare const directApi: {
|
|
|
175
206
|
eachBlock(inst: any, bindingId: any, deps: any, spec: any, regionId?: any): DocumentFragment;
|
|
176
207
|
};
|
|
177
208
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
209
|
+
* Function prop wire: only `onXxx` camelCase is a handler prop.
|
|
210
|
+
* Component `@event` must never normalize here — that channel is `onComponentEvent`.
|
|
180
211
|
* @param {unknown} name
|
|
181
212
|
* @returns {string | null}
|
|
182
213
|
*/
|
|
183
214
|
export declare function eventPropHandlerName(name: any): string;
|
|
215
|
+
/**
|
|
216
|
+
* Dispatch a component event to parent subscribers registered via `onComponentEvent`.
|
|
217
|
+
* Installed on every Direct instance as `inst.emit`.
|
|
218
|
+
* @param {object} inst
|
|
219
|
+
* @param {string} eventName
|
|
220
|
+
* @param {...any} payload
|
|
221
|
+
*/
|
|
222
|
+
export declare function emitComponentEvent(inst: any, eventName: any, ...payload: any[]): void;
|
|
184
223
|
export declare function isEventPropName(name: any): boolean;
|
|
185
224
|
/** HTML boolean attributes: presence means true; `false`/`null` must remove the attr. */
|
|
186
225
|
export declare const BOOLEAN_HTML_ATTRS: Set<string>;
|
package/dist/dom-core.js
CHANGED
|
@@ -262,6 +262,34 @@ export function runDirectCreate(Component, inst) {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
/** Host API for compiler-emitted `__vmzCreate` (direct path, Program IR B). */
|
|
265
|
+
/**
|
|
266
|
+
* First default `<slot>` owned by this component tree — skip nested `[data-vmz]`
|
|
267
|
+
* hosts (Button/Link/Empty label slots). `querySelector('slot')` steals those and
|
|
268
|
+
* projects parent siblings into the wrong host (commercial Drawer into Create btn).
|
|
269
|
+
* @param {Element | null | undefined} root
|
|
270
|
+
* @returns {Element | null}
|
|
271
|
+
*/
|
|
272
|
+
export function findOwnedDefaultSlot(root) {
|
|
273
|
+
if (!root || root.nodeType !== 1)
|
|
274
|
+
return null;
|
|
275
|
+
const tag = String(root.tagName || '').toLowerCase();
|
|
276
|
+
if (tag === 'slot' && !root.getAttribute('name'))
|
|
277
|
+
return root;
|
|
278
|
+
const kids = root.children;
|
|
279
|
+
if (!kids || !kids.length)
|
|
280
|
+
return null;
|
|
281
|
+
for (let i = 0; i < kids.length; i++) {
|
|
282
|
+
const c = kids[i];
|
|
283
|
+
if (c.nodeType !== 1)
|
|
284
|
+
continue;
|
|
285
|
+
if (c.hasAttribute('data-vmz'))
|
|
286
|
+
continue;
|
|
287
|
+
const hit = findOwnedDefaultSlot(c);
|
|
288
|
+
if (hit)
|
|
289
|
+
return hit;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
265
293
|
export const directApi = {
|
|
266
294
|
/** @type {object | null} */
|
|
267
295
|
_inst: null,
|
|
@@ -277,14 +305,52 @@ export const directApi = {
|
|
|
277
305
|
* }}
|
|
278
306
|
*/
|
|
279
307
|
_eachCtx: null,
|
|
308
|
+
/** @type {null | {
|
|
309
|
+
* el: (tag: string) => Element,
|
|
310
|
+
* text: (value?: any) => Text,
|
|
311
|
+
* componentHost?: (name: string) => HTMLElement | null,
|
|
312
|
+
* enter?: (node: Element) => boolean,
|
|
313
|
+
* leave?: () => void,
|
|
314
|
+
* scopeDepth?: () => number,
|
|
315
|
+
* rewindScope?: (depth: number) => void,
|
|
316
|
+
* beginBranchScope?: () => () => void,
|
|
317
|
+
* }} */
|
|
318
|
+
_resumeAdopt: null,
|
|
280
319
|
el(tag) {
|
|
320
|
+
if (directApi._resumeAdopt)
|
|
321
|
+
return directApi._resumeAdopt.el(tag);
|
|
281
322
|
noteDomCreate();
|
|
282
323
|
return document.createElement(tag || 'div');
|
|
283
324
|
},
|
|
284
325
|
text(value) {
|
|
326
|
+
if (directApi._resumeAdopt)
|
|
327
|
+
return directApi._resumeAdopt.text(value);
|
|
285
328
|
noteDomCreate();
|
|
286
329
|
return document.createTextNode(value == null ? '' : String(value));
|
|
287
330
|
},
|
|
331
|
+
/** Push adopted node's private resume pool for depth-first child create. */
|
|
332
|
+
adoptEnter(node) {
|
|
333
|
+
const adopt = directApi._resumeAdopt;
|
|
334
|
+
if (adopt && typeof adopt.enter === 'function') {
|
|
335
|
+
const ok = adopt.enter(node);
|
|
336
|
+
if (ok)
|
|
337
|
+
adopt._enterBalance = (adopt._enterBalance || 0) + 1;
|
|
338
|
+
return ok;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
},
|
|
342
|
+
adoptLeave() {
|
|
343
|
+
const adopt = directApi._resumeAdopt;
|
|
344
|
+
// Emit always pairs leave after enter; if enter failed (fresh node, no
|
|
345
|
+
// pool), do not pop — unbalanced leave previously stole parent scopes
|
|
346
|
+
// and projected siblings into nested Button slots.
|
|
347
|
+
if (!adopt || typeof adopt.leave !== 'function')
|
|
348
|
+
return;
|
|
349
|
+
if ((adopt._enterBalance || 0) <= 0)
|
|
350
|
+
return;
|
|
351
|
+
adopt._enterBalance -= 1;
|
|
352
|
+
adopt.leave();
|
|
353
|
+
},
|
|
288
354
|
frag() {
|
|
289
355
|
noteDomCreate();
|
|
290
356
|
return document.createDocumentFragment();
|
|
@@ -426,12 +492,21 @@ export const directApi = {
|
|
|
426
492
|
* @param {string | null} client
|
|
427
493
|
*/
|
|
428
494
|
component(hostInst, name, props, client) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
495
|
+
/** @type {HTMLElement | null} */
|
|
496
|
+
let host = null;
|
|
497
|
+
if (directApi._resumeAdopt && typeof directApi._resumeAdopt.componentHost === 'function') {
|
|
498
|
+
host = directApi._resumeAdopt.componentHost(name);
|
|
499
|
+
}
|
|
500
|
+
if (!host) {
|
|
501
|
+
noteDomCreate();
|
|
502
|
+
host = document.createElement('div');
|
|
503
|
+
host.setAttribute('data-vmz', name);
|
|
504
|
+
}
|
|
432
505
|
/** @type {Record<string, any>} */
|
|
433
506
|
const resolved = {};
|
|
434
507
|
for (const [k, v] of Object.entries(props || {})) {
|
|
508
|
+
// Function props that already look like `onXxx` stay as handlers.
|
|
509
|
+
// Component `@event` never arrives here (emit uses onComponentEvent).
|
|
435
510
|
const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
|
|
436
511
|
if (onKey)
|
|
437
512
|
resolved[onKey] = v;
|
|
@@ -471,9 +546,23 @@ export const directApi = {
|
|
|
471
546
|
if (!(Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function')) {
|
|
472
547
|
throw new Error(`vmz:dom direct component <${name}> requires __vmzCreate (rebuild child with Direct)`);
|
|
473
548
|
}
|
|
474
|
-
|
|
549
|
+
// Nested resume: keep parent `_resumeAdopt` so child reclaim parked SSR nodes.
|
|
550
|
+
// Enter the host's private child pool so nested create cannot see uncle siblings.
|
|
551
|
+
const adopt = directApi._resumeAdopt;
|
|
552
|
+
const entered = adopt && typeof adopt.enter === 'function' ? adopt.enter(host) : false;
|
|
553
|
+
let node;
|
|
554
|
+
try {
|
|
555
|
+
node = runDirectCreate(Ctor, child);
|
|
556
|
+
}
|
|
557
|
+
finally {
|
|
558
|
+
if (entered && adopt && typeof adopt.leave === 'function')
|
|
559
|
+
adopt.leave();
|
|
560
|
+
}
|
|
475
561
|
if (node) {
|
|
476
|
-
|
|
562
|
+
// ifBlock/eachBlock roots are DocumentFragments: appendChild moves
|
|
563
|
+
// children into `host` and empties the fragment. Keep a live Element
|
|
564
|
+
// root so projectDefaultSlot can find `<slot>` (Button v-if/v-else).
|
|
565
|
+
child.__vmzDomRoot = node.nodeType === 11 ? host : node;
|
|
477
566
|
host.appendChild(node);
|
|
478
567
|
}
|
|
479
568
|
host.__vmzInst = child;
|
|
@@ -487,6 +576,23 @@ export const directApi = {
|
|
|
487
576
|
}
|
|
488
577
|
return host;
|
|
489
578
|
},
|
|
579
|
+
/**
|
|
580
|
+
* Subscribe to a child component event (`@submit` → event name `submit`).
|
|
581
|
+
* Orthogonal to function props (`:on-submit` → prop `onSubmit`).
|
|
582
|
+
* @param {HTMLElement} hostEl
|
|
583
|
+
* @param {string} eventName
|
|
584
|
+
* @param {(ev?: any, ...rest: any[]) => any} handler
|
|
585
|
+
*/
|
|
586
|
+
onComponentEvent(hostEl, eventName, handler) {
|
|
587
|
+
const child = hostEl && hostEl.__vmzInst;
|
|
588
|
+
if (!child || typeof eventName !== 'string' || !eventName)
|
|
589
|
+
return;
|
|
590
|
+
if (typeof handler !== 'function')
|
|
591
|
+
return;
|
|
592
|
+
const bag = child.__vmzComponentListeners || (child.__vmzComponentListeners = Object.create(null));
|
|
593
|
+
const list = bag[eventName] || (bag[eventName] = []);
|
|
594
|
+
list.push(handler);
|
|
595
|
+
},
|
|
490
596
|
/**
|
|
491
597
|
* Keep nested Direct child props live with parent field writes.
|
|
492
598
|
* @param {object} hostInst
|
|
@@ -524,6 +630,7 @@ export const directApi = {
|
|
|
524
630
|
},
|
|
525
631
|
/**
|
|
526
632
|
* Project parent children into nested Direct component default `<slot>`.
|
|
633
|
+
* Uses {@link findOwnedDefaultSlot} so nested Button/Link slots are not stolen.
|
|
527
634
|
* @param {HTMLElement} hostEl
|
|
528
635
|
* @param {Node} node
|
|
529
636
|
*/
|
|
@@ -531,22 +638,20 @@ export const directApi = {
|
|
|
531
638
|
if (!hostEl || node == null)
|
|
532
639
|
return;
|
|
533
640
|
const child = hostEl.__vmzInst;
|
|
534
|
-
|
|
641
|
+
let root = (child && child.__vmzDomRoot) || hostEl;
|
|
642
|
+
// Emptied DocumentFragment after append must not receive slot kids.
|
|
643
|
+
if (!root || root.nodeType !== 1)
|
|
644
|
+
root = hostEl;
|
|
535
645
|
/** @type {Element | null} */
|
|
536
646
|
let slot = null;
|
|
537
647
|
if (root && root.nodeType === 1) {
|
|
538
|
-
|
|
539
|
-
slot = root;
|
|
540
|
-
}
|
|
541
|
-
else if (typeof root.querySelector === 'function') {
|
|
542
|
-
slot = root.querySelector('slot:not([name])');
|
|
543
|
-
}
|
|
648
|
+
slot = findOwnedDefaultSlot(root);
|
|
544
649
|
}
|
|
545
650
|
if (slot && slot.parentNode) {
|
|
546
651
|
slot.replaceWith(node);
|
|
547
652
|
return;
|
|
548
653
|
}
|
|
549
|
-
if (root && typeof root.appendChild === 'function')
|
|
654
|
+
if (root && root.nodeType === 1 && typeof root.appendChild === 'function')
|
|
550
655
|
root.appendChild(node);
|
|
551
656
|
else
|
|
552
657
|
hostEl.appendChild(node);
|
|
@@ -632,10 +737,14 @@ export const directApi = {
|
|
|
632
737
|
directApi._branchBinds = binds;
|
|
633
738
|
directApi._inst = inst;
|
|
634
739
|
let created = null;
|
|
740
|
+
const adopt = directApi._resumeAdopt;
|
|
741
|
+
const endBranch = adopt && typeof adopt.beginBranchScope === 'function' ? adopt.beginBranchScope() : null;
|
|
635
742
|
try {
|
|
636
743
|
created = branches[next].create.call(inst, directApi);
|
|
637
744
|
}
|
|
638
745
|
finally {
|
|
746
|
+
if (typeof endBranch === 'function')
|
|
747
|
+
endBranch();
|
|
639
748
|
directApi._branchBinds = prevSink;
|
|
640
749
|
directApi._inst = prevInst;
|
|
641
750
|
}
|
|
@@ -2646,8 +2755,8 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
|
|
|
2646
2755
|
trackDirectBind(inst, liveDeps, apply, bindingId);
|
|
2647
2756
|
}
|
|
2648
2757
|
/**
|
|
2649
|
-
*
|
|
2650
|
-
*
|
|
2758
|
+
* Function prop wire: only `onXxx` camelCase is a handler prop.
|
|
2759
|
+
* Component `@event` must never normalize here — that channel is `onComponentEvent`.
|
|
2651
2760
|
* @param {unknown} name
|
|
2652
2761
|
* @returns {string | null}
|
|
2653
2762
|
*/
|
|
@@ -2656,15 +2765,27 @@ export function eventPropHandlerName(name) {
|
|
|
2656
2765
|
return null;
|
|
2657
2766
|
if (/^on[A-Z]/.test(name))
|
|
2658
2767
|
return name;
|
|
2659
|
-
// `@input` → `onInput`; reject `@update:modelValue` until that surface exists.
|
|
2660
|
-
if (name.charAt(0) === '@' && name.length > 1 && !name.includes(':')) {
|
|
2661
|
-
const ev = name.slice(1);
|
|
2662
|
-
if (!ev || !/^[A-Za-z]/.test(ev))
|
|
2663
|
-
return null;
|
|
2664
|
-
return `on${ev.charAt(0).toUpperCase()}${ev.slice(1)}`;
|
|
2665
|
-
}
|
|
2666
2768
|
return null;
|
|
2667
2769
|
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Dispatch a component event to parent subscribers registered via `onComponentEvent`.
|
|
2772
|
+
* Installed on every Direct instance as `inst.emit`.
|
|
2773
|
+
* @param {object} inst
|
|
2774
|
+
* @param {string} eventName
|
|
2775
|
+
* @param {...any} payload
|
|
2776
|
+
*/
|
|
2777
|
+
export function emitComponentEvent(inst, eventName, ...payload) {
|
|
2778
|
+
if (!inst || typeof eventName !== 'string' || !eventName)
|
|
2779
|
+
return;
|
|
2780
|
+
const bag = inst.__vmzComponentListeners;
|
|
2781
|
+
const list = bag && bag[eventName];
|
|
2782
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
2783
|
+
return;
|
|
2784
|
+
for (const fn of list) {
|
|
2785
|
+
if (typeof fn === 'function')
|
|
2786
|
+
fn(...payload);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2668
2789
|
export function isEventPropName(name) {
|
|
2669
2790
|
return eventPropHandlerName(name) != null;
|
|
2670
2791
|
}
|
|
@@ -3047,6 +3168,11 @@ export function createInstance(Component, props = {}) {
|
|
|
3047
3168
|
inst.__vmzBinders = Object.create(null);
|
|
3048
3169
|
inst.__vmzBindings = Object.create(null);
|
|
3049
3170
|
inst.__vmzDepToBindings = Object.create(null);
|
|
3171
|
+
inst.__vmzComponentListeners = Object.create(null);
|
|
3172
|
+
// Compiler intrinsic surface: `this.emit('submit', payload)` — not a string bus.
|
|
3173
|
+
inst.emit = function emit(eventName, ...payload) {
|
|
3174
|
+
return emitComponentEvent(inst, eventName, ...payload);
|
|
3175
|
+
};
|
|
3050
3176
|
makeReactive(inst, Component.__vmzState || []);
|
|
3051
3177
|
makeReactive(inst, Component.__vmzProps || []);
|
|
3052
3178
|
// WriteBarrier: install Component helpers once (no import needed in emitted code).
|
package/dist/dom-ssr.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* VMZ DOM SSR / hydrate / resume — precise patches, no VDOM diff.
|
|
3
3
|
* Imports client DOM primitives from ./dom-core.js for tree-shakeable browser entry.
|
|
4
4
|
*/
|
|
5
|
+
import { findOwnedDefaultSlot } from './dom-core.js';
|
|
5
6
|
export declare function renderToString(Component: any, props?: {}, opts?: {}): Promise<any>;
|
|
6
7
|
/**
|
|
7
8
|
* Stream SSR via the same Direct serialize schedule as `renderToString`.
|
|
@@ -16,10 +17,11 @@ export declare function renderToStream(Component: any, props?: {}, opts?: {}): A
|
|
|
16
17
|
/**
|
|
17
18
|
* Live-DOM counterpart: first default `<slot>` owned by this tree, not by a nested
|
|
18
19
|
* `[data-vmz]` component (Button/Link labels, etc.).
|
|
20
|
+
* Re-exported from dom-core (single implementation).
|
|
19
21
|
* @param {Element | null | undefined} root
|
|
20
22
|
* @returns {Element | null}
|
|
21
23
|
*/
|
|
22
|
-
export
|
|
24
|
+
export { findOwnedDefaultSlot };
|
|
23
25
|
/**
|
|
24
26
|
* Hydrate/mount a file-route page inside an optional layout chain (outer → inner).
|
|
25
27
|
* Mirrors SSR `slotHtml` wrapping: each layout's owned default slot becomes the
|
package/dist/dom-ssr.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Imports client DOM primitives from ./dom-core.js for tree-shakeable browser entry.
|
|
5
5
|
*/
|
|
6
6
|
import { applyDirectHostBox, directHostBoxStyleAttr } from './direct-host-box.js';
|
|
7
|
-
import {
|
|
7
|
+
import { applyPreservedState, BOOLEAN_HTML_ATTRS, createInstance, destroy, directApi, eventPropHandlerName, findOwnedDefaultSlot, getRegisteredComponent, hasMeaningfulChild, isEventEntryStrategy, mount, noteDomCreate, resolveComponent, runDirectCreate, scheduleClientOn, settlePendingChildMounts, snapshotInstanceState, stripFns, } from './dom-core.js';
|
|
8
8
|
import { markUnknownComponentHost, serializeUnknownComponentNode } from './unknown-component.js';
|
|
9
9
|
/** @type {Error | null} last linkedom resolve failure (for clear SSR errors) */
|
|
10
10
|
let _ssrDocumentLastError = null;
|
|
@@ -185,31 +185,11 @@ function injectDefaultSlotHtml(node, html) {
|
|
|
185
185
|
/**
|
|
186
186
|
* Live-DOM counterpart: first default `<slot>` owned by this tree, not by a nested
|
|
187
187
|
* `[data-vmz]` component (Button/Link labels, etc.).
|
|
188
|
+
* Re-exported from dom-core (single implementation).
|
|
188
189
|
* @param {Element | null | undefined} root
|
|
189
190
|
* @returns {Element | null}
|
|
190
191
|
*/
|
|
191
|
-
export
|
|
192
|
-
if (!root || root.nodeType !== 1)
|
|
193
|
-
return null;
|
|
194
|
-
const tag = String(root.tagName || '').toLowerCase();
|
|
195
|
-
if (tag === 'slot' && !root.getAttribute('name'))
|
|
196
|
-
return root;
|
|
197
|
-
const kids = root.children;
|
|
198
|
-
if (!kids || !kids.length)
|
|
199
|
-
return null;
|
|
200
|
-
for (let i = 0; i < kids.length; i++) {
|
|
201
|
-
const c = kids[i];
|
|
202
|
-
if (c.nodeType !== 1)
|
|
203
|
-
continue;
|
|
204
|
-
// Nested component host — its slots are not the layout page outlet.
|
|
205
|
-
if (c.hasAttribute('data-vmz'))
|
|
206
|
-
continue;
|
|
207
|
-
const hit = findOwnedDefaultSlot(c);
|
|
208
|
-
if (hit)
|
|
209
|
-
return hit;
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
192
|
+
export { findOwnedDefaultSlot };
|
|
213
193
|
/**
|
|
214
194
|
* Hydrate/mount a file-route page inside an optional layout chain (outer → inner).
|
|
215
195
|
* Mirrors SSR `slotHtml` wrapping: each layout's owned default slot becomes the
|
|
@@ -592,6 +572,15 @@ const serializeApi = {
|
|
|
592
572
|
onMethod() {
|
|
593
573
|
/* named method events are also attached only during client resume */
|
|
594
574
|
},
|
|
575
|
+
adoptEnter() {
|
|
576
|
+
return false;
|
|
577
|
+
},
|
|
578
|
+
adoptLeave() {
|
|
579
|
+
/* resume scope only */
|
|
580
|
+
},
|
|
581
|
+
onComponentEvent() {
|
|
582
|
+
/* component events attach only on the client */
|
|
583
|
+
},
|
|
595
584
|
bindText(inst, bindingId, deps, get, textNode) {
|
|
596
585
|
let raw = '';
|
|
597
586
|
try {
|
|
@@ -620,13 +609,15 @@ const serializeApi = {
|
|
|
620
609
|
return;
|
|
621
610
|
// serializeApi.component returns a serialize el tree (or island shell).
|
|
622
611
|
const root = hostEl.__kind === 'el' ? hostEl : null;
|
|
623
|
-
const
|
|
612
|
+
const findOwnedSlot = (n) => {
|
|
624
613
|
if (!n || n.__kind !== 'el')
|
|
625
614
|
return null;
|
|
626
615
|
if (n.tag === 'slot' && !(n.attrs && n.attrs.name))
|
|
627
616
|
return n;
|
|
628
617
|
for (const c of n.children || []) {
|
|
629
|
-
|
|
618
|
+
if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
|
|
619
|
+
continue;
|
|
620
|
+
const hit = findOwnedSlot(c);
|
|
630
621
|
if (hit)
|
|
631
622
|
return hit;
|
|
632
623
|
}
|
|
@@ -636,12 +627,14 @@ const serializeApi = {
|
|
|
636
627
|
let slot = null;
|
|
637
628
|
if (root) {
|
|
638
629
|
for (const c of root.children || []) {
|
|
639
|
-
|
|
630
|
+
if (c && c.__kind === 'el' && c.attrs && c.attrs['data-vmz'] != null)
|
|
631
|
+
continue;
|
|
632
|
+
slot = findOwnedSlot(c);
|
|
640
633
|
if (slot)
|
|
641
634
|
break;
|
|
642
635
|
}
|
|
643
636
|
if (!slot)
|
|
644
|
-
slot =
|
|
637
|
+
slot = findOwnedSlot(root);
|
|
645
638
|
}
|
|
646
639
|
if (slot) {
|
|
647
640
|
slot.__rawHtml = null;
|
|
@@ -974,6 +967,167 @@ export function attachEventEntries(root = globalThis.document) {
|
|
|
974
967
|
});
|
|
975
968
|
}
|
|
976
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* Adopt parked SSR/Island nodes while `__vmzCreate` rebuilds the live tree.
|
|
972
|
+
* Children of `rootEl` are moved into `pool` first so the create schedule never
|
|
973
|
+
* leaves orphan SSR siblings beside if/each markers (duplicate h2 / stale text).
|
|
974
|
+
*
|
|
975
|
+
* Scope rules (sibling-host invariant):
|
|
976
|
+
* - Candidates are **direct children** of the current scope pool only (no deep
|
|
977
|
+
* walk into other parked siblings).
|
|
978
|
+
* - Adopting a node parks its children into a **private** `__vmzResumePool`.
|
|
979
|
+
* - `enter(node)` / `leave()` push that private pool for depth-first child create.
|
|
980
|
+
* - `beginBranchScope()` moves one top-level SSR node into an isolated pool so
|
|
981
|
+
* ifBlock branch create cannot steal parent siblings (Drawer / Button hosts).
|
|
982
|
+
*
|
|
983
|
+
* @param {DocumentFragment} pool
|
|
984
|
+
* @param {Element} rootEl
|
|
985
|
+
*/
|
|
986
|
+
function createResumeAdopt(pool, rootEl) {
|
|
987
|
+
const used = new WeakSet();
|
|
988
|
+
/** @type {DocumentFragment[]} */
|
|
989
|
+
const scopeStack = [pool];
|
|
990
|
+
/**
|
|
991
|
+
* @param {Node} root
|
|
992
|
+
* @param {(n: Text) => void} visit
|
|
993
|
+
*/
|
|
994
|
+
const walkTexts = (root, visit) => {
|
|
995
|
+
if (!root)
|
|
996
|
+
return;
|
|
997
|
+
if (root.nodeType === 3) {
|
|
998
|
+
visit(/** @type {Text} */ (root));
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (root.nodeType !== 1)
|
|
1002
|
+
return;
|
|
1003
|
+
for (let c = root.firstChild; c; c = c.nextSibling)
|
|
1004
|
+
walkTexts(c, visit);
|
|
1005
|
+
};
|
|
1006
|
+
const currentScope = () => scopeStack[scopeStack.length - 1] || pool;
|
|
1007
|
+
const markElement = (n) => {
|
|
1008
|
+
used.add(n);
|
|
1009
|
+
if (typeof n.getAttribute === 'function') {
|
|
1010
|
+
const k = n.getAttribute('data-vmz-key');
|
|
1011
|
+
if (k != null && n.__vmzKey == null)
|
|
1012
|
+
n.__vmzKey = k;
|
|
1013
|
+
}
|
|
1014
|
+
// Private child pool — never dump into the parent/global pool (that let
|
|
1015
|
+
// if branches reclaim Drawer/sibling hosts parked as uncles).
|
|
1016
|
+
const childPool = document.createDocumentFragment();
|
|
1017
|
+
while (n.firstChild)
|
|
1018
|
+
childPool.appendChild(n.firstChild);
|
|
1019
|
+
n.__vmzResumePool = childPool;
|
|
1020
|
+
return n;
|
|
1021
|
+
};
|
|
1022
|
+
/** @returns {Generator<Element>} */
|
|
1023
|
+
function* elementCandidates() {
|
|
1024
|
+
const scope = currentScope();
|
|
1025
|
+
if (scope === pool && rootEl && !used.has(rootEl))
|
|
1026
|
+
yield rootEl;
|
|
1027
|
+
// Snapshot — adopt may move nodes out of scope while iterating.
|
|
1028
|
+
const kids = [...scope.childNodes];
|
|
1029
|
+
for (const n of kids) {
|
|
1030
|
+
if (n.nodeType === 1 && !used.has(/** @type {Element} */ (n))) {
|
|
1031
|
+
yield /** @type {Element} */ (n);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/** @returns {Generator<Text>} */
|
|
1036
|
+
function* textCandidates() {
|
|
1037
|
+
const scope = currentScope();
|
|
1038
|
+
const kids = [...scope.childNodes];
|
|
1039
|
+
for (const n of kids) {
|
|
1040
|
+
/** @type {Text[]} */
|
|
1041
|
+
const found = [];
|
|
1042
|
+
walkTexts(n, (t) => {
|
|
1043
|
+
if (!used.has(t))
|
|
1044
|
+
found.push(t);
|
|
1045
|
+
});
|
|
1046
|
+
for (const t of found)
|
|
1047
|
+
yield t;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const adoptElement = (tag) => {
|
|
1051
|
+
const want = String(tag || 'div').toLowerCase();
|
|
1052
|
+
for (const el of elementCandidates()) {
|
|
1053
|
+
if (String(el.tagName).toLowerCase() === want)
|
|
1054
|
+
return markElement(el);
|
|
1055
|
+
}
|
|
1056
|
+
noteDomCreate();
|
|
1057
|
+
return document.createElement(tag || 'div');
|
|
1058
|
+
};
|
|
1059
|
+
const adoptText = (value) => {
|
|
1060
|
+
for (const n of textCandidates()) {
|
|
1061
|
+
used.add(n);
|
|
1062
|
+
if (value != null && value !== '')
|
|
1063
|
+
n.textContent = String(value);
|
|
1064
|
+
return n;
|
|
1065
|
+
}
|
|
1066
|
+
noteDomCreate();
|
|
1067
|
+
return document.createTextNode(value == null ? '' : String(value));
|
|
1068
|
+
};
|
|
1069
|
+
/** Reclaim SSR `<div data-vmz="Name">` hosts for nested Direct components. */
|
|
1070
|
+
const componentHost = (name) => {
|
|
1071
|
+
const want = String(name || '');
|
|
1072
|
+
for (const el of elementCandidates()) {
|
|
1073
|
+
if (String(el.tagName).toLowerCase() !== 'div')
|
|
1074
|
+
continue;
|
|
1075
|
+
if (el.getAttribute('data-vmz') !== want)
|
|
1076
|
+
continue;
|
|
1077
|
+
return markElement(el);
|
|
1078
|
+
}
|
|
1079
|
+
return null;
|
|
1080
|
+
};
|
|
1081
|
+
const enter = (node) => {
|
|
1082
|
+
if (!node || !node.__vmzResumePool)
|
|
1083
|
+
return false;
|
|
1084
|
+
scopeStack.push(node.__vmzResumePool);
|
|
1085
|
+
return true;
|
|
1086
|
+
};
|
|
1087
|
+
const leave = () => {
|
|
1088
|
+
if (scopeStack.length <= 1)
|
|
1089
|
+
return;
|
|
1090
|
+
scopeStack.pop();
|
|
1091
|
+
};
|
|
1092
|
+
const scopeDepth = () => scopeStack.length;
|
|
1093
|
+
const rewindScope = (depth) => {
|
|
1094
|
+
const d = Math.max(1, Number(depth) || 1);
|
|
1095
|
+
while (scopeStack.length > d)
|
|
1096
|
+
scopeStack.pop();
|
|
1097
|
+
};
|
|
1098
|
+
/**
|
|
1099
|
+
* Isolate the next top-level SSR sibling as the only candidates for an
|
|
1100
|
+
* if/else branch create (commercial Card: Empty branch must not adopt Drawer).
|
|
1101
|
+
* @returns {() => void} end callback
|
|
1102
|
+
*/
|
|
1103
|
+
const beginBranchScope = () => {
|
|
1104
|
+
const parent = currentScope();
|
|
1105
|
+
const branchPool = document.createDocumentFragment();
|
|
1106
|
+
for (const n of [...parent.childNodes]) {
|
|
1107
|
+
if (n.nodeType === 1 && !used.has(/** @type {Element} */ (n))) {
|
|
1108
|
+
branchPool.appendChild(n);
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
scopeStack.push(branchPool);
|
|
1113
|
+
const depth = scopeStack.length;
|
|
1114
|
+
return () => {
|
|
1115
|
+
while (branchPool.firstChild)
|
|
1116
|
+
branchPool.removeChild(branchPool.firstChild);
|
|
1117
|
+
rewindScope(depth - 1);
|
|
1118
|
+
};
|
|
1119
|
+
};
|
|
1120
|
+
return {
|
|
1121
|
+
el: adoptElement,
|
|
1122
|
+
text: adoptText,
|
|
1123
|
+
componentHost,
|
|
1124
|
+
enter,
|
|
1125
|
+
leave,
|
|
1126
|
+
scopeDepth,
|
|
1127
|
+
rewindScope,
|
|
1128
|
+
beginBranchScope,
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
977
1131
|
/**
|
|
978
1132
|
* Adopt existing Island DOM while running the same `__vmzCreate` schedule (resume).
|
|
979
1133
|
* @param {new (props?: object) => any} Component
|
|
@@ -985,50 +1139,32 @@ function runDirectResume(Component, inst, container) {
|
|
|
985
1139
|
if (!rootEl || rootEl.nodeType !== 1) {
|
|
986
1140
|
return runDirectCreate(Component, inst);
|
|
987
1141
|
}
|
|
988
|
-
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
},
|
|
1015
|
-
attr(el, name, value) {
|
|
1016
|
-
applyDomAttr(el, name, value);
|
|
1017
|
-
},
|
|
1018
|
-
on(el, type, handler) {
|
|
1019
|
-
el.addEventListener(type, handler);
|
|
1020
|
-
},
|
|
1021
|
-
bindText: directApi.bindText,
|
|
1022
|
-
bindAttr: directApi.bindAttr,
|
|
1023
|
-
bindComponentProp: directApi.bindComponentProp,
|
|
1024
|
-
projectDefaultSlot: directApi.projectDefaultSlot,
|
|
1025
|
-
setHtml: directApi.setHtml,
|
|
1026
|
-
bindHtml: directApi.bindHtml,
|
|
1027
|
-
ifBlock: directApi.ifBlock,
|
|
1028
|
-
eachBlock: directApi.eachBlock,
|
|
1029
|
-
component: directApi.component,
|
|
1030
|
-
};
|
|
1031
|
-
return Component.__vmzCreate.call(inst, api);
|
|
1142
|
+
// Park SSR descendants so create can append if/each structure without orphan siblings.
|
|
1143
|
+
const pool = document.createDocumentFragment();
|
|
1144
|
+
while (rootEl.firstChild)
|
|
1145
|
+
pool.appendChild(rootEl.firstChild);
|
|
1146
|
+
const prevInst = directApi._inst;
|
|
1147
|
+
const prevBranch = directApi._branchBinds;
|
|
1148
|
+
const prevItems = directApi._itemPatches;
|
|
1149
|
+
const prevEach = directApi._eachCtx;
|
|
1150
|
+
const prevAdopt = directApi._resumeAdopt;
|
|
1151
|
+
directApi._inst = inst;
|
|
1152
|
+
directApi._branchBinds = null;
|
|
1153
|
+
directApi._itemPatches = null;
|
|
1154
|
+
directApi._eachCtx = null;
|
|
1155
|
+
directApi._resumeAdopt = createResumeAdopt(pool, rootEl);
|
|
1156
|
+
try {
|
|
1157
|
+
return Component.__vmzCreate.call(inst, directApi);
|
|
1158
|
+
}
|
|
1159
|
+
finally {
|
|
1160
|
+
directApi._resumeAdopt = prevAdopt;
|
|
1161
|
+
directApi._inst = prevInst;
|
|
1162
|
+
directApi._branchBinds = prevBranch;
|
|
1163
|
+
directApi._itemPatches = prevItems;
|
|
1164
|
+
directApi._eachCtx = prevEach;
|
|
1165
|
+
while (pool.firstChild)
|
|
1166
|
+
pool.removeChild(pool.firstChild);
|
|
1167
|
+
}
|
|
1032
1168
|
}
|
|
1033
1169
|
/**
|
|
1034
1170
|
* @param {new (props?: object) => any} Component
|
|
@@ -1071,14 +1207,10 @@ export async function hydrate(Component, container, props = {}, opts = {}) {
|
|
|
1071
1207
|
}
|
|
1072
1208
|
}
|
|
1073
1209
|
else {
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const node = runDirectCreate(Component, inst);
|
|
1078
|
-
if (node) {
|
|
1210
|
+
// Deep adopt: park SSR children, rebuild schedule, reclaim nodes (no orphan siblings).
|
|
1211
|
+
const node = runDirectResume(Component, inst, container);
|
|
1212
|
+
if (node)
|
|
1079
1213
|
inst.__vmzDomRoot = node;
|
|
1080
|
-
container.appendChild(node);
|
|
1081
|
-
}
|
|
1082
1214
|
}
|
|
1083
1215
|
await settlePendingChildMounts(inst);
|
|
1084
1216
|
container.__vmzInst = inst;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Plan-native locale link rewrite for serve-host and static-emit.
|
|
3
|
+
* Href authority is RouteId × LocaleId realization rows — not path parsing on existing href.
|
|
4
4
|
*/
|
|
5
|
+
export declare const LOCALE_LINK_PLAN_SCHEMA = "vmz.static.locale_link_plan.v0";
|
|
5
6
|
export type LocaleHrefArtifact = {
|
|
6
7
|
locales?: Array<{
|
|
7
8
|
id: string;
|
|
@@ -12,8 +13,26 @@ export type LocaleHrefArtifact = {
|
|
|
12
13
|
defaultPrefix?: string;
|
|
13
14
|
defaultLocale?: string;
|
|
14
15
|
};
|
|
16
|
+
realizations?: Array<{
|
|
17
|
+
routeId: string;
|
|
18
|
+
localeId: string;
|
|
19
|
+
path: string;
|
|
20
|
+
}>;
|
|
15
21
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
export type LocaleLinkPlanRow = {
|
|
23
|
+
routeId: string;
|
|
24
|
+
localeId: string;
|
|
25
|
+
href: string;
|
|
26
|
+
};
|
|
27
|
+
export type LocaleLinkPlan = {
|
|
28
|
+
schema: typeof LOCALE_LINK_PLAN_SCHEMA;
|
|
29
|
+
rows: LocaleLinkPlanRow[];
|
|
30
|
+
};
|
|
31
|
+
/** Build RouteId × LocaleId → href rows from locale route realization artifact. */
|
|
32
|
+
export declare function buildLocaleLinkPlan(artifact: LocaleHrefArtifact | null | undefined): LocaleLinkPlan;
|
|
33
|
+
/** Apply plan rows to `<a data-vmz-route="RouteId">` markers for one LocaleId. */
|
|
34
|
+
export declare function applyLocaleLinkPlan(html: string, localeId: string, plan: LocaleLinkPlan, escapeAttr?: (s: string) => string): string;
|
|
35
|
+
/** Rewrite `<a data-vmz-route href>` using locale realization plan (Plan-native). */
|
|
19
36
|
export declare function localizeBodyLinks(html: string, localeId: string, artifact: LocaleHrefArtifact, escapeAttr?: (s: string) => string): string;
|
|
37
|
+
/** Legacy same-app href helper for callers without `data-vmz-route` markers. */
|
|
38
|
+
export declare function localizeSameAppHref(href: string, localeId: string, artifact: LocaleHrefArtifact): string;
|
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Plan-native locale link rewrite for serve-host and static-emit.
|
|
3
|
+
* Href authority is RouteId × LocaleId realization rows — not path parsing on existing href.
|
|
4
4
|
*/
|
|
5
|
+
export const LOCALE_LINK_PLAN_SCHEMA = 'vmz.static.locale_link_plan.v0';
|
|
6
|
+
/** Build RouteId × LocaleId → href rows from locale route realization artifact. */
|
|
7
|
+
export function buildLocaleLinkPlan(artifact) {
|
|
8
|
+
const rows = [];
|
|
9
|
+
for (const r of artifact?.realizations || []) {
|
|
10
|
+
if (!r?.routeId || !r?.localeId || !r?.path)
|
|
11
|
+
continue;
|
|
12
|
+
rows.push({ routeId: String(r.routeId), localeId: String(r.localeId), href: String(r.path) });
|
|
13
|
+
}
|
|
14
|
+
rows.sort((a, b) => (a.routeId === b.routeId ? a.localeId.localeCompare(b.localeId) : a.routeId.localeCompare(b.routeId)));
|
|
15
|
+
return { schema: LOCALE_LINK_PLAN_SCHEMA, rows };
|
|
16
|
+
}
|
|
17
|
+
/** Apply plan rows to `<a data-vmz-route="RouteId">` markers for one LocaleId. */
|
|
18
|
+
export function applyLocaleLinkPlan(html, localeId, plan, escapeAttr = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"')) {
|
|
19
|
+
if (!html || !localeId || !plan?.rows?.length)
|
|
20
|
+
return html;
|
|
21
|
+
const byRoute = new Map();
|
|
22
|
+
for (const row of plan.rows) {
|
|
23
|
+
if (row.localeId === localeId)
|
|
24
|
+
byRoute.set(row.routeId, row.href);
|
|
25
|
+
}
|
|
26
|
+
if (!byRoute.size)
|
|
27
|
+
return html;
|
|
28
|
+
return String(html).replace(/<a\b([^>]*)>/gi, (full, attrs) => {
|
|
29
|
+
const rm = attrs.match(/\bdata-vmz-route\s*=\s*"([^"]*)"/i);
|
|
30
|
+
if (!rm)
|
|
31
|
+
return full;
|
|
32
|
+
const href = byRoute.get(rm[1]);
|
|
33
|
+
if (!href)
|
|
34
|
+
return full;
|
|
35
|
+
const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
|
|
36
|
+
if (hm && hm[1] === href)
|
|
37
|
+
return full;
|
|
38
|
+
const newAttrs = hm ? attrs.replace(/\bhref\s*=\s*"[^"]*"/i, `href="${escapeAttr(href)}"`) : ` href="${escapeAttr(href)}"${attrs}`;
|
|
39
|
+
return `<a${newAttrs}>`;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Rewrite `<a data-vmz-route href>` using locale realization plan (Plan-native). */
|
|
43
|
+
export function localizeBodyLinks(html, localeId, artifact, escapeAttr) {
|
|
44
|
+
if (!html || !localeId || !artifact)
|
|
45
|
+
return html;
|
|
46
|
+
return applyLocaleLinkPlan(html, localeId, buildLocaleLinkPlan(artifact), escapeAttr);
|
|
47
|
+
}
|
|
5
48
|
function normalizePath(pathname) {
|
|
6
49
|
let p = String(pathname || '/');
|
|
7
50
|
if (!p.startsWith('/'))
|
|
@@ -10,32 +53,7 @@ function normalizePath(pathname) {
|
|
|
10
53
|
p = p.slice(0, -1);
|
|
11
54
|
return p || '/';
|
|
12
55
|
}
|
|
13
|
-
|
|
14
|
-
const parts = normalizePath(pathname).split('/').filter(Boolean);
|
|
15
|
-
if (!parts.length)
|
|
16
|
-
return { localeId: null, restPath: '/' };
|
|
17
|
-
if (supportedLocales.includes(parts[0])) {
|
|
18
|
-
const rest = parts.slice(1);
|
|
19
|
-
return { localeId: parts[0], restPath: rest.length ? `/${rest.join('/')}` : '/' };
|
|
20
|
-
}
|
|
21
|
-
return { localeId: null, restPath: normalizePath(pathname) };
|
|
22
|
-
}
|
|
23
|
-
function realizeRoutePath(localeId, pathPattern, routing) {
|
|
24
|
-
const strategy = routing.strategy || 'prefix';
|
|
25
|
-
const defaultPrefix = routing.defaultPrefix || 'include';
|
|
26
|
-
const defaultLocale = routing.defaultLocale;
|
|
27
|
-
const base = normalizePath(pathPattern);
|
|
28
|
-
if (strategy === 'none' || strategy === 'domain') {
|
|
29
|
-
return { path: base };
|
|
30
|
-
}
|
|
31
|
-
const omitDefault = defaultPrefix === 'omit' && localeId === defaultLocale;
|
|
32
|
-
if (omitDefault) {
|
|
33
|
-
return { path: base };
|
|
34
|
-
}
|
|
35
|
-
const pathOut = base === '/' ? `/${localeId}` : `/${localeId}${base}`;
|
|
36
|
-
return { path: pathOut };
|
|
37
|
-
}
|
|
38
|
-
/** Rewrite a same-app href to the given LocaleId via route realization. */
|
|
56
|
+
/** Legacy same-app href helper for callers without `data-vmz-route` markers. */
|
|
39
57
|
export function localizeSameAppHref(href, localeId, artifact) {
|
|
40
58
|
if (!href || !localeId || !artifact)
|
|
41
59
|
return href;
|
|
@@ -43,6 +61,7 @@ export function localizeSameAppHref(href, localeId, artifact) {
|
|
|
43
61
|
return href;
|
|
44
62
|
if (/^[a-z][a-z0-9+.-]*:/i.test(href) && !href.startsWith('/'))
|
|
45
63
|
return href;
|
|
64
|
+
const plan = buildLocaleLinkPlan(artifact);
|
|
46
65
|
let pathname = String(href);
|
|
47
66
|
let search = '';
|
|
48
67
|
let hash = '';
|
|
@@ -56,34 +75,12 @@ export function localizeSameAppHref(href, localeId, artifact) {
|
|
|
56
75
|
search = pathname.slice(qIdx);
|
|
57
76
|
pathname = pathname.slice(0, qIdx);
|
|
58
77
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
68
|
-
const parsed = parseLocaleFromPath(pathname, supported);
|
|
69
|
-
const rest = parsed.restPath || '/';
|
|
70
|
-
const realized = realizeRoutePath(localeId, rest, routing);
|
|
71
|
-
return `${realized.path}${search}${hash}`;
|
|
72
|
-
}
|
|
73
|
-
/** Rewrite `<a data-vmz-route href>` in HTML body to retain `localeId`. */
|
|
74
|
-
export function localizeBodyLinks(html, localeId, artifact, escapeAttr = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"')) {
|
|
75
|
-
if (!html || !localeId || !artifact)
|
|
76
|
-
return html;
|
|
77
|
-
return String(html).replace(/<a\b([^>]*)>/gi, (full, attrs) => {
|
|
78
|
-
if (!/\bdata-vmz-route\s*=/.test(attrs))
|
|
79
|
-
return full;
|
|
80
|
-
const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
|
|
81
|
-
if (!hm)
|
|
82
|
-
return full;
|
|
83
|
-
const next = localizeSameAppHref(hm[1], localeId, artifact);
|
|
84
|
-
if (next === hm[1])
|
|
85
|
-
return full;
|
|
86
|
-
const newAttrs = attrs.replace(/\bhref\s*=\s*"[^"]*"/i, `href="${escapeAttr(next)}"`);
|
|
87
|
-
return `<a${newAttrs}>`;
|
|
88
|
-
});
|
|
78
|
+
pathname = normalizePath(pathname || '/');
|
|
79
|
+
for (const row of plan.rows) {
|
|
80
|
+
if (row.localeId !== localeId)
|
|
81
|
+
continue;
|
|
82
|
+
if (normalizePath(row.href) === pathname)
|
|
83
|
+
return `${row.href}${search}${hash}`;
|
|
84
|
+
}
|
|
85
|
+
return href;
|
|
89
86
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
|
|
6
6
|
"exports": {
|
|
@@ -66,12 +66,12 @@
|
|
|
66
66
|
"linkedom": "^0.18.13"
|
|
67
67
|
},
|
|
68
68
|
"optionalDependencies": {
|
|
69
|
-
"@vmz/vmz-win32-x64": "0.1.
|
|
70
|
-
"@vmz/vmz-win32-arm64": "0.1.
|
|
71
|
-
"@vmz/vmz-darwin-x64": "0.1.
|
|
72
|
-
"@vmz/vmz-darwin-arm64": "0.1.
|
|
73
|
-
"@vmz/vmz-linux-x64": "0.1.
|
|
74
|
-
"@vmz/vmz-linux-arm64": "0.1.
|
|
69
|
+
"@vmz/vmz-win32-x64": "0.1.26",
|
|
70
|
+
"@vmz/vmz-win32-arm64": "0.1.26",
|
|
71
|
+
"@vmz/vmz-darwin-x64": "0.1.26",
|
|
72
|
+
"@vmz/vmz-darwin-arm64": "0.1.26",
|
|
73
|
+
"@vmz/vmz-linux-x64": "0.1.26",
|
|
74
|
+
"@vmz/vmz-linux-arm64": "0.1.26"
|
|
75
75
|
},
|
|
76
76
|
"publishConfig": {
|
|
77
77
|
"access": "public"
|