fix: replace empty text nodes with zero width joiners (#140)

This commit is contained in:
Luke LaValva 2023-02-02 19:30:52 -05:00 committed by GitHub
parent 00e9076ca1
commit 2cfef7b89e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 233 additions and 75 deletions

View File

@ -7,63 +7,63 @@
{
"name": "*",
"total": {
"min": 12770,
"gzip": 5389,
"brotli": 4906
"min": 12741,
"gzip": 5401,
"brotli": 4909
}
},
{
"name": "counter",
"user": {
"min": 369,
"gzip": 282,
"brotli": 243
"gzip": 285,
"brotli": 272
},
"runtime": {
"min": 3183,
"gzip": 1505,
"brotli": 1346
"min": 3214,
"gzip": 1528,
"brotli": 1372
},
"total": {
"min": 3552,
"gzip": 1787,
"brotli": 1589
"min": 3583,
"gzip": 1813,
"brotli": 1644
}
},
{
"name": "counter 💧",
"user": {
"min": 207,
"gzip": 182,
"gzip": 181,
"brotli": 153
},
"runtime": {
"min": 2691,
"gzip": 1354,
"brotli": 1210
"min": 2722,
"gzip": 1382,
"brotli": 1229
},
"total": {
"min": 2898,
"gzip": 1536,
"brotli": 1363
"min": 2929,
"gzip": 1563,
"brotli": 1382
}
},
{
"name": "comments",
"user": {
"min": 1158,
"gzip": 708,
"brotli": 638
"min": 1163,
"gzip": 710,
"brotli": 636
},
"runtime": {
"min": 7257,
"gzip": 3277,
"brotli": 2973
"min": 7284,
"gzip": 3291,
"brotli": 2990
},
"total": {
"min": 8415,
"gzip": 3985,
"brotli": 3611
"min": 8447,
"gzip": 4001,
"brotli": 3626
}
},
{
@ -71,17 +71,17 @@
"user": {
"min": 949,
"gzip": 592,
"brotli": 543
"brotli": 545
},
"runtime": {
"min": 8306,
"gzip": 3738,
"brotli": 3389
"min": 8333,
"gzip": 3753,
"brotli": 3397
},
"total": {
"min": 9255,
"gzip": 4330,
"brotli": 3932
"min": 9282,
"gzip": 4345,
"brotli": 3942
}
}
]

View File

@ -114,16 +114,12 @@ export function props(scope: Scope, nodeIndex: number, index: number) {
scope[index + "-"] = nextProps;
}
export function innerHTML(element: Element, value: string) {
element.innerHTML = normalizeString(value);
}
function normalizeAttrValue(value: unknown) {
return value == null || value === false ? undefined : value + "";
return value || value === 0 ? value + "" : undefined;
}
function normalizeString(value: unknown) {
return value == null ? "" : value + "";
return value || value === 0 ? value + "" : "\u200d";
}
type EffectFn<S extends Scope> = (scope: S) => void | (() => void);

View File

@ -11,7 +11,6 @@ export {
export {
data,
html,
innerHTML,
attr,
attrs,
classAttr,

View File

@ -32,10 +32,11 @@ function applyValue<S extends Scope, V>(
value: V,
valueAccessor: Accessor,
subscribers: Signal[],
isCreate: boolean,
action?: (scope: S, value: V) => void
) {
const stale = write(scope, valueAccessor as number, value);
if (stale) {
if (stale || isCreate) {
action?.(scope, value);
}
notifySubscribers(scope, stale as any as boolean, subscribers);
@ -66,8 +67,16 @@ export function source<S extends Scope, V>(
}
},
___apply(scope, data) {
const isCreate = scope[markAccessor] === undefined;
scope[markAccessor] = 1;
applyValue(scope as S, data as V, valueAccessor, subscribers, action);
applyValue(
scope as S,
data as V,
valueAccessor,
subscribers,
isCreate,
action
);
scope[markAccessor] = 0;
},
};
@ -201,7 +210,14 @@ export function derivation<S extends Scope, V>(
subscribers,
defaultMark,
(scope: S) => {
applyValue(scope, compute(scope), valueAccessor, subscribers, action);
applyValue(
scope,
compute(scope),
valueAccessor,
subscribers,
false, // TODO: This should be true on creation
action
);
}
);
if (MARKO_DEBUG) {

View File

@ -1,5 +1,5 @@
export function toString(val: unknown) {
return val == null ? "" : val + "";
return val || val === 0 ? val + "" : "";
}
export const escapeXML = escapeIfNeeded((val: string) => {
@ -82,14 +82,15 @@ export function escapeAttrValue(val: string) {
function escapeIfNeeded(escape: (val: string) => string) {
return (val: unknown) => {
if (val == null) {
return "";
if (!val && val !== 0) {
return "&zwj;";
}
switch (typeof val) {
case "string":
return escape(val);
case "boolean":
return "true";
case "number":
return val + "";
default:

View File

@ -0,0 +1,14 @@
# Render {}
```html
<div>
</div>
```
# Render "ASYNC"
```html
<div>
Client Only
</div>
```

View File

@ -0,0 +1,24 @@
# Render {}
```html
<div>
</div>
```
# Mutations
```
inserted div0
```
# Render "ASYNC"
```html
<div>
Client Only
</div>
```
# Mutations
```
div0/#text0: "" => "Client Only"
```

View File

@ -0,0 +1,11 @@
import { setSource as _setSource, queueSource as _queueSource, data as _data, source as _source, register as _register, queueHydrate as _queueHydrate, createRenderFn as _createRenderFn } from "@marko/runtime-fluurt/src/dom";
const _x = /* @__PURE__ */_source("x", [], (_scope, x) => _data(_scope["#text/0"], x));
const _hydrate_setup = _register("packages/translator/src/__tests__/fixtures/let-undefined-until-dom/template.marko_0", _scope => _queueSource(_scope, _x, "Client Only"));
const _setup = _scope => {
_setSource(_scope, _x, undefined);
_queueHydrate(_scope, _hydrate_setup);
};
export const template = "<div> </div>";
export const walks = /* next(1), get, out(1) */"D l";
export const setup = _setup;
export default /* @__PURE__ */_createRenderFn(template, walks, setup, null, null, "packages/translator/src/__tests__/fixtures/let-undefined-until-dom/template.marko");

View File

@ -0,0 +1,9 @@
import { escapeXML as _escapeXML, markHydrateNode as _markHydrateNode, write as _write, nextScopeId as _nextScopeId, writeHydrateCall as _writeHydrateCall, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
const x = undefined;
_write(`<div>${_escapeXML(x)}${_markHydrateNode(_scope0_id, "#text/0")}</div>`);
_writeHydrateCall(_scope0_id, "packages/translator/src/__tests__/fixtures/let-undefined-until-dom/template.marko_0");
}, "packages/translator/src/__tests__/fixtures/let-undefined-until-dom/template.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,14 @@
# Render {}
```html
<div>
</div>
```
# Render "ASYNC"
```html
<div>
Client Only
</div>
```

View File

@ -0,0 +1,42 @@
# Render {}
```html
<html>
<head />
<body>
<div>
<!--M#0 #text/0-->
</div>
<script>
(M$h=[]).push(null,[0,"packages/translator/src/__tests__/fixtures/let-undefined-until-dom/template.marko_0",])
</script>
</body>
</html>
```
# Mutations
```
```
# Render "ASYNC"
```html
<html>
<head />
<body>
<div>
Client Only
<!--M#0 #text/0-->
</div>
<script>
(M$h=[]).push(null,[0,"packages/translator/src/__tests__/fixtures/let-undefined-until-dom/template.marko_0",])
</script>
</body>
</html>
```
# Mutations
```
#document/html0/body1/div0/#text0: "" => "Client Only"
```

View File

@ -0,0 +1,6 @@
# Render "End"
```html
<div>
</div>
```

View File

@ -0,0 +1,31 @@
# Write
<div>&zwj;<!M#0 #text/0></div><script>(M$h=[]).push(null,[0,"packages/translator/src/__tests__/fixtures/let-undefined-until-dom/template.marko_0",])</script>
# Render "End"
```html
<html>
<head />
<body>
<div>
<!--M#0 #text/0-->
</div>
<script>
(M$h=[]).push(null,[0,"packages/translator/src/__tests__/fixtures/let-undefined-until-dom/template.marko_0",])
</script>
</body>
</html>
```
# Mutations
```
inserted #document/html0
inserted #document/html0/head0
inserted #document/html0/body1
inserted #document/html0/body1/div0
inserted #document/html0/body1/div0/#text0
inserted #document/html0/body1/div0/#comment1
inserted #document/html0/body1/script1
inserted #document/html0/body1/script1/#text0
```

View File

@ -0,0 +1,6 @@
<let/x/>
<effect() {
x = "Client Only"
}/>
<div>${x}</div>

View File

@ -0,0 +1,3 @@
import { wait } from "../../utils/resolve";
export const steps = [{}, wait(1)];

View File

@ -5,7 +5,7 @@
<span>
0
</span>
, was=false
, was=
</div>
<button
id="increment"

View File

@ -5,7 +5,7 @@
<span>
0
</span>
, was=false
, was=
</div>
<button
id="increment"
@ -41,7 +41,7 @@ container.querySelector("#increment")?.click();
# Mutations
```
div0/span1/#text0: "0" => "1"
div0/#text3: "false" => "0"
div0/#text3: "" => "0"
```

View File

@ -5,7 +5,7 @@
<span>
0
</span>
, was=false
, was=
</div>
<button
id="increment"

View File

@ -11,7 +11,7 @@
</span>
, was=
<!---->
false
<!--M#0 #text/1-->
</div>
<button
@ -67,7 +67,7 @@ container.querySelector("#increment")?.click();
# Mutations
```
#document/html0/body1/div0/span1/#text0: "0" => "1"
#document/html0/body1/div0/#text4: "false" => "0"
#document/html0/body1/div0/#text4: "" => "0"
```

View File

@ -5,7 +5,7 @@
<span>
0
</span>
, was=false
, was=
</div>
<button
id="increment"

View File

@ -1,5 +1,5 @@
# Write
<div>x=<span>0<!M#0 #text/0></span>, was=<!>false<!M#0 #text/1></div><button id=increment>Increment</button><!M#0 #button/2><script>(M$h=[]).push((b,s)=>({0:{x:0}}),[0,"packages/translator/src/__tests__/fixtures/lifecycle-tag-assignment/template.marko_0_x",])</script>
<div>x=<span>0<!M#0 #text/0></span>, was=<!>&zwj;<!M#0 #text/1></div><button id=increment>Increment</button><!M#0 #button/2><script>(M$h=[]).push((b,s)=>({0:{x:0}}),[0,"packages/translator/src/__tests__/fixtures/lifecycle-tag-assignment/template.marko_0_x",])</script>
# Render "End"
@ -15,7 +15,7 @@
</span>
, was=
<!---->
false
<!--M#0 #text/1-->
</div>
<button

View File

@ -13,7 +13,11 @@ export default {
translate(tag) {
const { node } = tag;
const tagVar = node.var;
const [defaultAttr] = node.attributes;
const defaultAttr =
node.attributes.find(
(attr) =>
t.isMarkoAttribute(attr) && (attr.default || attr.name === "value")
) ?? t.markoAttribute("value", t.identifier("undefined"));
assertNoParams(tag);
assertNoBodyContent(tag);
@ -30,24 +34,6 @@ export default {
.buildCodeFrameError("The 'let' cannot be destructured.");
}
if (!defaultAttr) {
throw tag
.get("name")
.buildCodeFrameError("The 'let' tag requires a default attribute.");
}
if (
node.attributes.length > 1 ||
!t.isMarkoAttribute(defaultAttr) ||
(!defaultAttr.default && defaultAttr.name !== "default")
) {
throw tag
.get("name")
.buildCodeFrameError(
"The 'let' tag only supports the 'default' attribute."
);
}
if (isOutputDOM()) {
const sectionId = getSectionId(tag);
const binding = tagVar.extra.reserve!;