@vmz/core 0.3.13 → 0.3.15

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.
@@ -1,6 +1,9 @@
1
- import { directHostBoxStyleAttr } from '../browser/direct-host-box.js';
1
+ import { directHostBoxStyleAttr, resolveDirectHostTag } from '../browser/direct-host-box.js';
2
2
  import { BOOLEAN_HTML_ATTRS, createInstance, eventPropHandlerName, getRegisteredComponent, isEventEntryStrategy, mergeClassParts, mergeStyleParts, snapshotInstanceState, stripFns, } from '../browser/dom-core.js';
3
3
  import { serializeUnknownComponentNode } from '../browser/unknown-component.js';
4
+ import { splitStyleDeclarations } from '../shared/dom-attr-normalize.js';
5
+ import { HTML_HASH_ATTR, htmlDigest } from '../shared/html-digest.js';
6
+ import { rowEndMarker, rowMarker } from '../shared/row-key.js';
4
7
  import { _ssrDocumentLastError, ensureSsrDocument } from './document.js';
5
8
  import { awaitSsrOnMount, ssrComponentName, ssrOnMountContext } from './onmount.js';
6
9
  function runDirectSerializeTree(Component, inst) {
@@ -56,16 +59,122 @@ const VOID_TAGS = new Set([
56
59
  function isVoidTag(tag) {
57
60
  return VOID_TAGS.has(String(tag).toLowerCase());
58
61
  }
62
+ /**
63
+ * `textarea` carries its initial value as text content and `select` carries it
64
+ * as a selected option, so neither can travel as a `value` attribute. `input`
65
+ * keeps the attribute, which browsers honor for the default value.
66
+ */
67
+ function controlValue(node, tag) {
68
+ if (tag !== 'textarea' && tag !== 'select')
69
+ return null;
70
+ const v = node.attrs ? node.attrs.value : null;
71
+ if (v == null || v === false)
72
+ return null;
73
+ // A `multiple` select model is a list; keep every entry addressable.
74
+ if (tag === 'select' && Array.isArray(v))
75
+ return v.map((item) => String(item));
76
+ return String(v);
77
+ }
78
+ function nodeText(node) {
79
+ if (!node || typeof node !== 'object')
80
+ return '';
81
+ if (node.__kind === 'text')
82
+ return String(node.value ?? '');
83
+ if (node.__kind === 'el' || node.__kind === 'frag') {
84
+ return (node.children || []).map(nodeText).join('');
85
+ }
86
+ return '';
87
+ }
88
+ /** Mark the `<option>` whose value (or text) equals one of the model values. */
89
+ function markSelectSelection(node, wanted) {
90
+ const wants = Array.isArray(wanted) ? wanted.map((item) => String(item)) : [String(wanted)];
91
+ const visit = (n) => {
92
+ if (!n || typeof n !== 'object')
93
+ return;
94
+ if (n.__kind === 'el') {
95
+ const tag = String(n.tag || '').toLowerCase();
96
+ if (tag === 'option') {
97
+ const raw = n.attrs && n.attrs.value != null ? String(n.attrs.value) : nodeText(n).trim();
98
+ if (wants.includes(raw)) {
99
+ if (!n.attrs)
100
+ n.attrs = {};
101
+ n.attrs.selected = '';
102
+ }
103
+ else if (n.attrs && Object.prototype.hasOwnProperty.call(n.attrs, 'selected')) {
104
+ delete n.attrs.selected;
105
+ }
106
+ return;
107
+ }
108
+ }
109
+ for (const c of n.children || [])
110
+ visit(c);
111
+ };
112
+ for (const c of node.children || [])
113
+ visit(c);
114
+ }
115
+ /**
116
+ * Inline style as the browser would serialize it: the `style` attribute merged
117
+ * with the live `style.*` writes that Direct patches and `v-show` perform
118
+ * (`el.style.display = …`). SSR virtual elements only kept the attribute, so a
119
+ * server-first response showed elements that hydration hid.
120
+ */
121
+ function serializeStyleAttr(node) {
122
+ const declared = node.attrs && node.attrs.style != null ? String(node.attrs.style) : '';
123
+ const live = node.style;
124
+ if (!live || typeof live !== 'object')
125
+ return declared;
126
+ const display = live.display;
127
+ if (display === undefined)
128
+ return declared;
129
+ // `el.style.display = ''` removes the declaration instead of writing it.
130
+ if (display === '' || display == null) {
131
+ return splitStyleDeclarations(declared)
132
+ .filter((chunk) => {
133
+ const idx = chunk.indexOf(':');
134
+ return idx < 0 || chunk.slice(0, idx).trim() !== 'display';
135
+ })
136
+ .map((chunk) => chunk.trim())
137
+ .filter(Boolean)
138
+ .join('; ');
139
+ }
140
+ return mergeStyleParts(declared, { display });
141
+ }
59
142
  function serializeOpenTag(node) {
60
143
  const tag = node.tag || 'div';
144
+ const lowerTag = String(tag).toLowerCase();
145
+ // `v-pre`: the authored attribute names are the output. Skipping the four
146
+ // special branches here is what keeps `className` / `style` / `value` /
147
+ // `data-vmz-html-hash` literal inside a verbatim subtree.
148
+ const verbatim = node.__vmzVerbatim === true;
61
149
  let attrs = '';
62
150
  for (const [k, v] of Object.entries(node.attrs || {})) {
63
151
  if (v == null || v === false)
64
152
  continue;
65
- if (k === 'className')
66
- attrs += ` class="${escapeHtml(v)}"`;
67
- else
68
- attrs += ` ${k}="${escapeHtml(v)}"`;
153
+ if (!verbatim) {
154
+ // `textarea` value renders as text content and `select` value as a
155
+ // selected option, so the raw attribute must not be emitted.
156
+ if (k === 'value' && (lowerTag === 'textarea' || lowerTag === 'select'))
157
+ continue;
158
+ if (k === 'style')
159
+ continue;
160
+ // Always replaced by the digest of the payload written below, so an
161
+ // author-supplied marker can never mask a different payload.
162
+ if (k === HTML_HASH_ATTR)
163
+ continue;
164
+ if (k === 'className') {
165
+ attrs += ` class="${escapeHtml(v)}"`;
166
+ continue;
167
+ }
168
+ }
169
+ attrs += ` ${k}="${escapeHtml(v)}"`;
170
+ }
171
+ const style = verbatim ? '' : serializeStyleAttr(node);
172
+ if (style)
173
+ attrs += ` style="${escapeHtml(style)}"`;
174
+ // `v-html` hosts carry the digest of the exact string SSR wrote, so the
175
+ // client can keep the SSR subtree without DOM-serializing its payload.
176
+ if (node.__rawHtml != null && !node.__rawOuter) {
177
+ attrs += ` ${HTML_HASH_ATTR}="${escapeHtml(htmlDigest(String(node.__rawHtml)))}"`;
69
178
  }
70
179
  return { tag, open: `<${tag}${attrs}>` };
71
180
  }
@@ -81,7 +190,14 @@ export function flattenSerializeNode(node) {
81
190
  if (node.__kind === 'frag') {
82
191
  if (node.__rawHtml != null)
83
192
  return String(node.__rawHtml);
84
- return (node.children || []).map(flattenSerializeNode).join('');
193
+ const inner = (node.children || []).map(flattenSerializeNode).join('');
194
+ // Multi-root `<template v-for>` rows have no element root to carry
195
+ // `data-vmz-key`, so their identity travels as a marker pair around the
196
+ // row's nodes. Resume reads the same pair and scopes adoption to it.
197
+ if (node.__vmzRowKey != null) {
198
+ return `<!--${rowMarker(node.__vmzRowKey)}-->${inner}<!--${rowEndMarker(node.__vmzRowKey)}-->`;
199
+ }
200
+ return inner;
85
201
  }
86
202
  if (node.__kind === 'el') {
87
203
  const tag = node.tag || 'div';
@@ -94,11 +210,22 @@ export function flattenSerializeNode(node) {
94
210
  if (node.__rawOuter && node.__rawHtml != null)
95
211
  return String(node.__rawHtml);
96
212
  const { open } = serializeOpenTag(node);
213
+ const lowerTag = String(tag).toLowerCase();
214
+ const control = controlValue(node, lowerTag);
97
215
  if (node.__rawHtml != null) {
98
- return isVoidTag(tag) ? `${open}${String(node.__rawHtml)}` : `${open}${String(node.__rawHtml)}</${tag}>`;
216
+ const lead = needsSacrificialLF(node, lowerTag, null) ? '\n' : '';
217
+ const raw = String(node.__rawHtml);
218
+ return isVoidTag(tag) ? `${open}${raw}` : `${open}${lead}${raw}</${tag}>`;
219
+ }
220
+ if (lowerTag === 'textarea' && control != null) {
221
+ const lead = needsSacrificialLF(node, lowerTag, control) ? '\n' : '';
222
+ return `${open}${lead}${escapeHtml(control)}</${tag}>`;
99
223
  }
224
+ if (lowerTag === 'select' && control != null)
225
+ markSelectSelection(node, control);
100
226
  const inner = (node.children || []).map(flattenSerializeNode).join('');
101
- return isVoidTag(tag) ? `${open}${inner}` : `${open}${inner}</${tag}>`;
227
+ const lead = needsSacrificialLF(node, lowerTag, control) ? '\n' : '';
228
+ return isVoidTag(tag) ? `${open}${inner}` : `${open}${lead}${inner}</${tag}>`;
102
229
  }
103
230
  return '';
104
231
  }
@@ -120,6 +247,13 @@ export function* streamSerializeChunks(node) {
120
247
  yield String(node.__rawHtml);
121
248
  return;
122
249
  }
250
+ if (node.__vmzRowKey != null) {
251
+ yield `<!--${rowMarker(node.__vmzRowKey)}-->`;
252
+ for (const c of node.children || [])
253
+ yield* streamSerializeChunks(c);
254
+ yield `<!--${rowEndMarker(node.__vmzRowKey)}-->`;
255
+ return;
256
+ }
123
257
  for (const c of node.children || [])
124
258
  yield* streamSerializeChunks(c);
125
259
  return;
@@ -141,10 +275,20 @@ export function* streamSerializeChunks(node) {
141
275
  }
142
276
  const { open } = serializeOpenTag(node);
143
277
  yield open;
278
+ const lowerTag = String(tag).toLowerCase();
279
+ const control = controlValue(node, lowerTag);
280
+ const lead = needsSacrificialLF(node, lowerTag, control) ? '\n' : '';
144
281
  if (node.__rawHtml != null) {
145
- yield String(node.__rawHtml);
282
+ yield lead + String(node.__rawHtml);
283
+ }
284
+ else if (lowerTag === 'textarea' && control != null) {
285
+ yield lead + escapeHtml(control);
146
286
  }
147
287
  else {
288
+ if (lowerTag === 'select' && control != null)
289
+ markSelectSelection(node, control);
290
+ if (lead)
291
+ yield lead;
148
292
  for (const c of node.children || [])
149
293
  yield* streamSerializeChunks(c);
150
294
  }
@@ -274,6 +418,16 @@ function virtualRemove(node) {
274
418
  p.children.splice(idx, 1);
275
419
  node.parentNode = null;
276
420
  }
421
+ /**
422
+ * Replace an element's content with escaped text (`v-text` / `el.textContent`).
423
+ * Direct emit writes `textContent` for both static and reactive text, so SSR
424
+ * must model it or the first response ships an empty element.
425
+ */
426
+ function replaceSerializeText(node, value) {
427
+ const text = value == null ? '' : String(value);
428
+ node.__rawHtml = null;
429
+ node.children = text === '' ? [] : [{ __kind: 'text', value: text, parentNode: node }];
430
+ }
277
431
  function makeVirtualEl(tag) {
278
432
  const node = {
279
433
  __kind: 'el',
@@ -282,6 +436,7 @@ function makeVirtualEl(tag) {
282
436
  children: [],
283
437
  style: {},
284
438
  __rawHtml: null,
439
+ __vmzKey: null,
285
440
  parentNode: null,
286
441
  setAttribute(name, value) {
287
442
  applySerializeAttr(this, name, value);
@@ -305,6 +460,17 @@ function makeVirtualEl(tag) {
305
460
  this.children = [];
306
461
  },
307
462
  });
463
+ // Direct emit writes `el.textContent` for v-text (static `api.setText` and
464
+ // reactive `el.textContent = …`). Mirror it so the serializer sees the text.
465
+ Object.defineProperty(node, 'textContent', {
466
+ configurable: true,
467
+ get() {
468
+ return nodeText(this);
469
+ },
470
+ set(value) {
471
+ replaceSerializeText(this, value);
472
+ },
473
+ });
308
474
  return node;
309
475
  }
310
476
  /**
@@ -383,11 +549,19 @@ const serializeApi = {
383
549
  serializeApi._ssrCollected.push(child);
384
550
  return child;
385
551
  },
386
- el(tag) {
552
+ el(tag, key) {
387
553
  const node = makeVirtualEl(tag);
388
554
  if (String(tag || '').toLowerCase() === 'svg') {
389
555
  node.namespaceURI = 'http://www.w3.org/2000/svg';
390
556
  }
557
+ // Row roots need their key in the HTML so hydration reclaims by identity,
558
+ // not the order in which tags happen to appear.
559
+ if (key != null) {
560
+ node.__vmzKey = key;
561
+ if (!node.attrs)
562
+ node.attrs = Object.create(null);
563
+ node.attrs['data-vmz-key'] = String(key);
564
+ }
391
565
  return node;
392
566
  },
393
567
  elNS(ns, tag) {
@@ -426,6 +600,16 @@ const serializeApi = {
426
600
  },
427
601
  };
428
602
  },
603
+ /** Multi-root `<template v-for>` row fragment; serializes its marker pair. */
604
+ fragRow(key) {
605
+ const frag = serializeApi.frag();
606
+ if (key != null)
607
+ frag.__vmzRowKey = String(key);
608
+ return frag;
609
+ },
610
+ rowEnd() {
611
+ /* SSR rows are one-shot: the marker pair closes at serialization. */
612
+ },
429
613
  insertBefore(parent, node, ref) {
430
614
  virtualInsertBefore(parent, node, ref);
431
615
  },
@@ -475,6 +659,15 @@ const serializeApi = {
475
659
  attr(el, name, value) {
476
660
  if (!el || el.__kind !== 'el')
477
661
  return;
662
+ // `v-pre`: keep the authored name and value — no `className` → `class`
663
+ // rewrite, no class/style merge, no boolean-attribute normalization.
664
+ if (el.__vmzVerbatim) {
665
+ if (value == null || value === false)
666
+ delete el.attrs[name];
667
+ else
668
+ el.attrs[name] = value === true ? '' : String(value);
669
+ return;
670
+ }
478
671
  applySerializeAttr(el, name, value);
479
672
  },
480
673
  mergeClass(...parts) {
@@ -492,6 +685,9 @@ const serializeApi = {
492
685
  adoptEnter() {
493
686
  return false;
494
687
  },
688
+ adoptEnterSlot() {
689
+ return false;
690
+ },
495
691
  adoptLeave() {
496
692
  /* resume scope only */
497
693
  },
@@ -529,6 +725,26 @@ const serializeApi = {
529
725
  bindComponentProp() {
530
726
  /* SSR: props already resolved into the child instance at create */
531
727
  },
728
+ bindComponentProps() {
729
+ /* SSR: `...api.spreadProps()` already delivered the object props at create */
730
+ },
731
+ bindComponentEvents() {
732
+ /* component events attach only on the client */
733
+ },
734
+ bindObjectListeners() {
735
+ /* object listeners attach only on the client */
736
+ },
737
+ spreadProps(source) {
738
+ const out = Object.create(null);
739
+ if (source && typeof source === 'object') {
740
+ for (const key of Object.keys(source)) {
741
+ if (!key || key.startsWith('#'))
742
+ continue;
743
+ out[key] = source[key];
744
+ }
745
+ }
746
+ return out;
747
+ },
532
748
  projectDefaultSlot(hostEl, node) {
533
749
  if (!hostEl || node == null)
534
750
  return;
@@ -552,8 +768,13 @@ const serializeApi = {
552
768
  target.__rawHtml = null;
553
769
  if (!Array.isArray(target.children))
554
770
  target.children = [];
555
- // Append — multiple projectDefaultSlot calls must accumulate (SSR).
556
- // Client path replaces the live <slot> then appends siblings; serialize must push.
771
+ // The client replaces the live `<slot>` with the first projected node,
772
+ // which discards its fallback children. Drop the fallback on the first
773
+ // projection too, then accumulate later projections as siblings.
774
+ if (!filledSerializeSlots.has(target)) {
775
+ filledSerializeSlots.add(target);
776
+ target.children.length = 0;
777
+ }
557
778
  target.children.push(node);
558
779
  return;
559
780
  }
@@ -612,6 +833,23 @@ const serializeApi = {
612
833
  el.__rawHtml = value == null ? '' : String(value);
613
834
  el.children = [];
614
835
  },
836
+ // Emitted `v-html` write. SSR always records the payload, and the open tag
837
+ // gains `data-vmz-html-hash` from that exact string at serialize time.
838
+ html(el, value) {
839
+ serializeApi.setHtml(el, value);
840
+ },
841
+ setText(el, value) {
842
+ if (!el || el.__kind !== 'el')
843
+ return;
844
+ replaceSerializeText(el, value);
845
+ },
846
+ setShow(el, condition) {
847
+ if (!el || el.__kind !== 'el')
848
+ return;
849
+ if (!el.style || typeof el.style !== 'object')
850
+ el.style = {};
851
+ el.style.display = condition ? '' : 'none';
852
+ },
615
853
  bindHtml(inst, bindingId, deps, get, el) {
616
854
  serializeApi.trackPatch(inst, deps || [], function bindHtmlPatch() {
617
855
  let raw;
@@ -624,14 +862,20 @@ const serializeApi = {
624
862
  serializeApi.setHtml(el, raw);
625
863
  }, bindingId);
626
864
  },
627
- component(hostInst, nameOrCtor, props, client) {
865
+ component(hostInst, nameOrCtor, props, client, key) {
628
866
  const name = typeof nameOrCtor === 'function' ? nameOrCtor.__vmzTag || nameOrCtor.name || 'Component' : nameOrCtor;
629
867
  const Ctor = typeof nameOrCtor === 'function' ? nameOrCtor : getRegisteredComponent(name);
630
868
  if (!Ctor) {
631
869
  return serializeUnknownComponentNode(name);
632
870
  }
871
+ const rawProps = (props || {});
872
+ // `id` / `data-*` / `aria-*` belong on the host element, matching the
873
+ // browser `api.component` fallthrough.
874
+ const hostAttrs = rawProps.__hostAttrs;
633
875
  const resolved = {};
634
- for (const [k, v] of Object.entries(props || {})) {
876
+ for (const [k, v] of Object.entries(rawProps)) {
877
+ if (k === '__hostAttrs')
878
+ continue;
635
879
  const onKey = typeof v === 'function' ? eventPropHandlerName(k) : null;
636
880
  if (onKey)
637
881
  continue;
@@ -640,6 +884,16 @@ const serializeApi = {
640
884
  else
641
885
  resolved[k] = v;
642
886
  }
887
+ const applyHostAttrs = (attrs) => {
888
+ if (!hostAttrs)
889
+ return attrs;
890
+ for (const [k, v] of Object.entries(hostAttrs)) {
891
+ if (v == null || v === false)
892
+ continue;
893
+ attrs[k] = v === true ? '' : String(v);
894
+ }
895
+ return attrs;
896
+ };
643
897
  if (client) {
644
898
  // resume: Island SSR includes body + ResumeEntry slice (same Direct schedule).
645
899
  const child = serializeApi._ssrChildInstance(Ctor, resolved);
@@ -675,12 +929,15 @@ const serializeApi = {
675
929
  const boxStyle = directHostBoxStyleAttr(name, Ctor);
676
930
  if (boxStyle)
677
931
  attrs.style = boxStyle;
932
+ applyHostAttrs(attrs);
933
+ if (key != null)
934
+ attrs['data-vmz-key'] = String(key);
678
935
  if (isEventEntryStrategy(String(client))) {
679
936
  attrs['data-vmz-entry'] = 'event';
680
937
  }
681
938
  return {
682
939
  __kind: 'el',
683
- tag: 'div',
940
+ tag: resolveDirectHostTag(name, Ctor),
684
941
  attrs,
685
942
  children: body ? [body] : [],
686
943
  appendChild(c) {
@@ -705,9 +962,12 @@ const serializeApi = {
705
962
  const boxStyle = directHostBoxStyleAttr(name, Ctor);
706
963
  if (boxStyle)
707
964
  attrs.style = boxStyle;
965
+ applyHostAttrs(attrs);
966
+ if (key != null)
967
+ attrs['data-vmz-key'] = String(key);
708
968
  return {
709
969
  __kind: 'el',
710
- tag: 'div',
970
+ tag: resolveDirectHostTag(name, Ctor),
711
971
  attrs,
712
972
  children: node ? [node] : [],
713
973
  appendChild(c) {
@@ -723,6 +983,28 @@ const serializeApi = {
723
983
  }
724
984
  throw new Error(`vmz:dom serialize component <${name}> requires __vmzCreate (rebuild child with Direct)`);
725
985
  },
986
+ /**
987
+ * `<component :is>` on the server: `is` and props are evaluated once at
988
+ * create time, so the dynamic boundary serializes exactly like the resolved
989
+ * static component.
990
+ */
991
+ dynamicComponent(hostInst, bindingId, deps, getCtor, getProps, key) {
992
+ let Ctor = null;
993
+ try {
994
+ Ctor = getCtor.call(hostInst);
995
+ }
996
+ catch {
997
+ Ctor = null;
998
+ }
999
+ let props = {};
1000
+ try {
1001
+ props = getProps.call(hostInst) || {};
1002
+ }
1003
+ catch {
1004
+ props = {};
1005
+ }
1006
+ return serializeApi.component(hostInst, Ctor, props, null, key);
1007
+ },
726
1008
  };
727
1009
  function applySerializeAttr(el, name, value) {
728
1010
  if (!el || el.__kind !== 'el')
@@ -751,11 +1033,69 @@ function applySerializeAttr(el, name, value) {
751
1033
  el.attrs[key] = value === true ? '' : String(value);
752
1034
  return;
753
1035
  }
1036
+ // A `multiple` select model is a list. Stringifying it here would emit one
1037
+ // `value="c,a"` attribute, so `markSelectSelection` could never match an
1038
+ // option and SSR shipped a select with nothing selected.
1039
+ if (key === 'value' && String(el.tag || '').toLowerCase() === 'select' && Array.isArray(value)) {
1040
+ el.attrs[key] = value.map((item) => String(item));
1041
+ return;
1042
+ }
754
1043
  if (value == null || value === false)
755
1044
  delete el.attrs[key];
756
1045
  else
757
1046
  el.attrs[key] = value === true ? '' : String(value);
758
1047
  }
759
1048
  function escapeHtml(s) {
760
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1049
+ return String(s)
1050
+ .replace(/&/g, '&amp;')
1051
+ .replace(/</g, '&lt;')
1052
+ .replace(/>/g, '&gt;')
1053
+ .replace(/"/g, '&quot;')
1054
+ // The HTML input stream normalizes a literal CR (and CRLF) to LF before
1055
+ // tokenization, so a raw `\r` would not survive the parse. Emit a character
1056
+ // reference instead: hydration then keeps the exact server value.
1057
+ .replace(/\r/g, '&#13;');
1058
+ }
1059
+ /** `<pre>` / `<textarea>` drop one leading LF after their start tag (HTML spec). */
1060
+ function dropsLeadingLF(tag) {
1061
+ return tag === 'pre' || tag === 'textarea';
1062
+ }
1063
+ /** Does the first token inside `node` start with LF? Comments and tags do not. */
1064
+ function firstTokenStartsWithLF(node) {
1065
+ if (node == null || node === false)
1066
+ return false;
1067
+ if (typeof node === 'string')
1068
+ return node.startsWith('\n');
1069
+ if (node.__kind === 'text')
1070
+ return String(node.value ?? '').startsWith('\n');
1071
+ if (node.__kind === 'comment')
1072
+ return false;
1073
+ if (node.__kind === 'frag') {
1074
+ if (node.__rawHtml != null)
1075
+ return String(node.__rawHtml).startsWith('\n');
1076
+ for (const c of node.children || []) {
1077
+ if (c == null || c === false)
1078
+ continue;
1079
+ return firstTokenStartsWithLF(c);
1080
+ }
1081
+ }
1082
+ return false;
1083
+ }
1084
+ /**
1085
+ * A value that itself starts with LF needs one sacrificial newline: the parser
1086
+ * discards the first LF token, leaving the real one in the DOM.
1087
+ */
1088
+ function needsSacrificialLF(node, lowerTag, control) {
1089
+ if (!dropsLeadingLF(lowerTag))
1090
+ return false;
1091
+ if (lowerTag === 'textarea' && control != null)
1092
+ return String(control).startsWith('\n');
1093
+ if (node.__rawHtml != null)
1094
+ return String(node.__rawHtml).startsWith('\n');
1095
+ for (const c of node.children || []) {
1096
+ if (c == null || c === false)
1097
+ continue;
1098
+ return firstTokenStartsWithLF(c);
1099
+ }
1100
+ return false;
761
1101
  }
package/package.json CHANGED
@@ -68,12 +68,12 @@
68
68
  "main": "./dist/faces/server.js",
69
69
  "name": "@vmz/core",
70
70
  "optionalDependencies": {
71
- "@vmz/vmz-darwin-arm64": "0.3.13",
72
- "@vmz/vmz-darwin-x64": "0.3.13",
73
- "@vmz/vmz-linux-arm64": "0.3.13",
74
- "@vmz/vmz-linux-x64": "0.3.13",
75
- "@vmz/vmz-win32-arm64": "0.3.13",
76
- "@vmz/vmz-win32-x64": "0.3.13"
71
+ "@vmz/vmz-darwin-arm64": "0.3.15",
72
+ "@vmz/vmz-darwin-x64": "0.3.15",
73
+ "@vmz/vmz-linux-arm64": "0.3.15",
74
+ "@vmz/vmz-linux-x64": "0.3.15",
75
+ "@vmz/vmz-win32-arm64": "0.3.15",
76
+ "@vmz/vmz-win32-x64": "0.3.15"
77
77
  },
78
78
  "publishConfig": {
79
79
  "access": "public"
@@ -88,5 +88,5 @@
88
88
  },
89
89
  "type": "module",
90
90
  "types": "./dist/faces/index.d.ts",
91
- "version": "0.3.13"
91
+ "version": "0.3.15"
92
92
  }