fix: regression with text only elements with no children and a spread

This commit is contained in:
dpiercey 2025-12-05 08:52:05 -07:00 committed by Dylan Piercey
parent 04902fad7d
commit 8cdeb6bb1d
14 changed files with 204 additions and 61 deletions

View File

@ -0,0 +1,5 @@
---
"@marko/runtime-tags": patch
---
Fix regression with text only tags (textarea/title) with a spread and no body content.

View File

@ -0,0 +1,8 @@
{
"vars": {
"props": {
"$_": "r",
"$init": "m"
}
}
}

View File

@ -0,0 +1,7 @@
# Render `{"value":"foo"}`
```html
<textarea>
foo
</textarea>
```

View File

@ -0,0 +1,12 @@
# Render `{"value":"foo"}`
```html
<textarea>
foo
</textarea>
```
# Mutations
```
INSERT textarea
```

View File

@ -0,0 +1,2 @@
// size: 46 (min) 50 (brotli)
(_._script("a0", ($scope) => _._attrs_script($scope, "a")), init());

View File

@ -0,0 +1,10 @@
export const $template = "<textarea></textarea>";
export const $walks = /* get, over(1) */" b";
export const $setup = () => {};
import * as _ from "@marko/runtime-tags/debug/dom";
const $input__script = _._script("__tests__/template.marko_0_input", $scope => _._attrs_script($scope, "#textarea/0"));
export const $input = /* @__PURE__ */_._const("input", $scope => {
_._attrs($scope, "#textarea/0", $scope.input);
$input__script($scope);
});
export default /* @__PURE__ */_._template("__tests__/template.marko", $template, $walks, $setup, $input);

View File

@ -0,0 +1,12 @@
import * as _ from "@marko/runtime-tags/debug/html";
export default _._template("__tests__/template.marko", input => {
const $scope0_id = _._scope_id();
const $textarea_input = input;
_._html(`<textarea${_._attrs($textarea_input, "#textarea/0", $scope0_id, "textarea")}>${_._attr_textarea_value($scope0_id, "#textarea/0", $textarea_input.value, $textarea_input.valueChange)}</textarea>${_._el_resume($scope0_id, "#textarea/0")}`);
_._script($scope0_id, "__tests__/template.marko_0_input");
_._scope($scope0_id, {
input
}, "__tests__/template.marko", 0, {
input: 0
});
});

View File

@ -0,0 +1,7 @@
# Render `{"value":"foo"}`
```html
<textarea>
foo
</textarea>
```

View File

@ -0,0 +1,26 @@
# Render `{"value":"foo"}`
```html
<html>
<head />
<body>
<textarea>
foo
</textarea>
<!--M_*1 #textarea/0-->
<script>
WALKER_RUNTIME("M")("_");
M._.r = [_ =&gt; (_.a = [0,
{
input:
{
value: "foo"
}
}]),
"__tests__/template.marko_0_input 1"
];
M._.w()
</script>
</body>
</html>
```

View File

@ -0,0 +1,6 @@
# Render End
```html
<textarea>
foo
</textarea>
```

View File

@ -0,0 +1,42 @@
# Write
```html
<textarea>foo</textarea><!--M_*1 #textarea/0--><script>WALKER_RUNTIME("M")("_");M._.r=[_=>(_.a=[0,{input:{value:"foo"}}]),"__tests__/template.marko_0_input 1"];M._.w()</script>
```
# Render End
```html
<html>
<head />
<body>
<textarea>
foo
</textarea>
<!--M_*1 #textarea/0-->
<script>
WALKER_RUNTIME("M")("_");
M._.r = [_ =&gt; (_.a = [0,
{
input:
{
value: "foo"
}
}]),
"__tests__/template.marko_0_input 1"
];
M._.w()
</script>
</body>
</html>
```
# Mutations
```
INSERT html
INSERT html/head
INSERT html/body
INSERT html/body/textarea
INSERT html/body/textarea/#text
INSERT html/body/#comment
INSERT html/body/script
INSERT html/body/script/#text
```

View File

@ -0,0 +1 @@
<textarea ...input/>

View File

@ -0,0 +1 @@
export const steps = [{ value: "foo" }];

View File

@ -89,7 +89,7 @@ export default {
}
const tagName = getCanonicalTagName(tag);
const textOnly = isTextOnlyNativeTag(tag);
const isTextOnly = isTextOnlyNativeTag(tag);
const seen: Record<string, t.MarkoAttribute> = {};
const { attributes } = tag.node;
let injectNonce = isInjectNonceTag(tagName);
@ -146,7 +146,7 @@ export default {
});
let textPlaceholders: undefined | t.Node[];
if (textOnly) {
if (isTextOnly) {
for (const child of tag.node.body.body) {
if (t.isMarkoPlaceholder(child)) {
(textPlaceholders ||= []).push(child.value);
@ -372,12 +372,13 @@ export default {
}
const isOpenOnly = !!(tagDef && tagDef.parseOptions?.openTagOnly);
const isTextOnly = isTextOnlyNativeTag(tag);
const hasChildren = !!tag.node.body.body.length;
if (spreadExpression) {
addHTMLEffectCall(tagSection, tagExtra.referencedBindings);
if (isOpenOnly || hasChildren || staticContentAttr) {
if (isTextOnly || isOpenOnly || hasChildren || staticContentAttr) {
if (skipExpression) {
write`${callRuntime(
"_attrs_partial",
@ -409,59 +410,59 @@ export default {
write`>`;
break;
}
} else {
if (staticContentAttr) {
write`>`;
tagExtra[kTagContentAttr] = true;
(tag.node.body.body as t.Statement[]) = [
t.expressionStatement(
callRuntime(
"_attr_content",
visitAccessor,
getScopeIdIdentifier(tagSection),
staticContentAttr.value,
getSerializeGuard(
tagSection,
nodeBinding && getSerializeReason(tagSection, nodeBinding),
true,
),
} else if (isTextOnly) {
write`>`;
} else if (staticContentAttr) {
write`>`;
tagExtra[kTagContentAttr] = true;
(tag.node.body.body as t.Statement[]) = [
t.expressionStatement(
callRuntime(
"_attr_content",
visitAccessor,
getScopeIdIdentifier(tagSection),
staticContentAttr.value,
getSerializeGuard(
tagSection,
nodeBinding && getSerializeReason(tagSection, nodeBinding),
true,
),
),
];
} else if (spreadExpression && !hasChildren) {
const serializeReason = getSerializeGuard(
tagSection,
nodeBinding && getSerializeReason(tagSection, nodeBinding),
true,
);
tagExtra[kTagContentAttr] = true;
(tag.node.body.body as t.Statement[]) = [
skipExpression
? t.expressionStatement(
callRuntime(
"_attrs_partial_content",
spreadExpression,
skipExpression,
visitAccessor,
getScopeIdIdentifier(tagSection),
t.stringLiteral(tagName),
serializeReason,
),
)
: t.expressionStatement(
callRuntime(
"_attrs_content",
spreadExpression,
visitAccessor,
getScopeIdIdentifier(tagSection),
t.stringLiteral(tagName),
serializeReason,
),
),
];
} else if (spreadExpression && !hasChildren) {
const serializeReason = getSerializeGuard(
tagSection,
nodeBinding && getSerializeReason(tagSection, nodeBinding),
true,
);
tagExtra[kTagContentAttr] = true;
(tag.node.body.body as t.Statement[]) = [
skipExpression
? t.expressionStatement(
callRuntime(
"_attrs_partial_content",
spreadExpression,
skipExpression,
visitAccessor,
getScopeIdIdentifier(tagSection),
t.stringLiteral(tagName),
serializeReason,
),
];
} else {
write`>`;
}
)
: t.expressionStatement(
callRuntime(
"_attrs_content",
spreadExpression,
visitAccessor,
getScopeIdIdentifier(tagSection),
t.stringLiteral(tagName),
serializeReason,
),
),
];
} else {
write`>`;
}
if (writeAtStartOfBody) {
@ -471,8 +472,8 @@ export default {
exit(tag) {
const tagExtra = tag.node.extra!;
const nodeBinding = tagExtra[kNativeTagBinding];
const openTagOnly = getTagDef(tag)?.parseOptions?.openTagOnly;
const textOnly = isTextOnlyNativeTag(tag);
const isOpenOnly = getTagDef(tag)?.parseOptions?.openTagOnly;
const isTextOnly = isTextOnlyNativeTag(tag);
const selectArgs = htmlSelectArgs.get(tag.node);
const tagName = getCanonicalTagName(tag);
const tagSection = getSection(tag);
@ -510,7 +511,7 @@ export default {
),
),
);
} else if (textOnly) {
} else if (isTextOnly) {
for (const child of tag.node.body.body) {
if (t.isMarkoText(child)) {
write`${child.value}`;
@ -522,7 +523,7 @@ export default {
tag.insertBefore(tag.node.body.body).forEach((child) => child.skip());
}
if (!tagExtra[kSkipEndTag] && !openTagOnly && !selectArgs) {
if (!tagExtra[kSkipEndTag] && !isOpenOnly && !selectArgs) {
write`</${tagName}>`;
}
@ -559,6 +560,7 @@ export default {
injectNonce,
} = getUsedAttrs(tagName, tag.node);
const isOpenOnly = !!(tagDef && tagDef.parseOptions?.openTagOnly);
const isTextOnly = isTextOnlyNativeTag(tag);
const hasChildren = !!tag.node.body.body.length;
if (injectNonce) {
@ -712,6 +714,7 @@ export default {
if (spreadExpression) {
const canHaveAttrContent = !(
isTextOnly ||
isOpenOnly ||
hasChildren ||
staticContentAttr
@ -796,13 +799,14 @@ export default {
const tagExtra = tag.node.extra!;
const nodeBinding = tagExtra[kNativeTagBinding];
const openTagOnly = getTagDef(tag)?.parseOptions?.openTagOnly;
const textOnly = isTextOnlyNativeTag(tag);
const tagName = getCanonicalTagName(tag);
if (!openTagOnly) {
if (textOnly) {
const write = writer.writeTo(tag);
if (tagName !== "textarea" && isTextOnlyNativeTag(tag)) {
const textLiteral = bodyToTextLiteral(tag.node.body);
if (t.isStringLiteral(textLiteral)) {
writer.writeTo(tag)`${textLiteral}`;
write`${textLiteral}`;
} else {
addStatement(
"render",
@ -823,7 +827,7 @@ export default {
.forEach((child) => child.skip());
}
writer.writeTo(tag)`</${getCanonicalTagName(tag)}>`;
write`</${tagName}>`;
}
walks.exit(tag);