Skip to content

Commit 858da76

Browse files
committed
fix: correct selectedcontent parsing in customizable selects
- handle selectedcontent as void element only with /> syntax - support button closing tags in select context - prevent incorrect nesting of option elements
1 parent 89c80fe commit 858da76

File tree

7 files changed

+148
-16
lines changed

7 files changed

+148
-16
lines changed

internal/parser.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,6 +1418,14 @@ func inBodyIM(p *parser) bool {
14181418
return true
14191419
}
14201420
default:
1421+
// Special handling for selectedcontent as a void element
1422+
if p.tok.Data == "selectedcontent" {
1423+
p.reconstructActiveFormattingElements()
1424+
p.addElement()
1425+
p.oe.pop()
1426+
p.acknowledgeSelfClosingTag()
1427+
return true
1428+
}
14211429
p.reconstructActiveFormattingElements()
14221430
p.addElement()
14231431
if p.hasSelfClosingToken {
@@ -1436,6 +1444,12 @@ func inBodyIM(p *parser) bool {
14361444
return true
14371445
}
14381446

1447+
// Special handling for selectedcontent end tag - just ignore it
1448+
// since it's treated as a void element
1449+
if p.tok.Data == "selectedcontent" {
1450+
return true
1451+
}
1452+
14391453
switch p.tok.DataAtom {
14401454
case a.Body:
14411455
p.addLoc()
@@ -2319,6 +2333,19 @@ func inSelectIM(p *parser) bool {
23192333
p.resetInsertionMode()
23202334
case a.Template:
23212335
return inHeadIM(p)
2336+
default:
2337+
// Handle closing tags for elements that are allowed in customizable select
2338+
// (like button for the new HTML select element)
2339+
if p.tok.Data == "button" {
2340+
// Close the button if it's open
2341+
for i := len(p.oe) - 1; i >= 0; i-- {
2342+
if p.oe[i].Data == "button" {
2343+
p.oe = p.oe[:i]
2344+
break
2345+
}
2346+
}
2347+
return true
2348+
}
23222349
}
23232350
case CommentToken:
23242351
p.addChild(&Node{
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
[TestPrinter/selectedcontent_element_in_customizable_select - 1]
2+
## Input
3+
4+
```
5+
<select><button><selectedcontent></selectedcontent></button><option>Option 1</option><option>Option 2</option></select>
6+
```
7+
8+
## Output
9+
10+
```js
11+
import {
12+
Fragment,
13+
render as $$render,
14+
createAstro as $$createAstro,
15+
createComponent as $$createComponent,
16+
renderComponent as $$renderComponent,
17+
renderHead as $$renderHead,
18+
maybeRenderHead as $$maybeRenderHead,
19+
unescapeHTML as $$unescapeHTML,
20+
renderSlot as $$renderSlot,
21+
mergeSlots as $$mergeSlots,
22+
addAttribute as $$addAttribute,
23+
spreadAttributes as $$spreadAttributes,
24+
defineStyleVars as $$defineStyleVars,
25+
defineScriptVars as $$defineScriptVars,
26+
renderTransition as $$renderTransition,
27+
createTransitionScope as $$createTransitionScope,
28+
renderScript as $$renderScript,
29+
createMetadata as $$createMetadata
30+
} from "http://localhost:3000/";
31+
32+
export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: [], hydrationDirectives: new Set([]), hoisted: [] });
33+
34+
const $$Component = $$createComponent(($$result, $$props, $$slots) => {
35+
36+
return $$render`${$$maybeRenderHead($$result)}<select><button><selectedcontent></button><option>Option 1</option><option>Option 2</option></select>`;
37+
}, undefined, undefined);
38+
export default $$Component;
39+
```
40+
---
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
[TestPrinter/selectedcontent_self-closing_element - 1]
2+
## Input
3+
4+
```
5+
<select><button><selectedcontent /></button><option>Option 1</option><option>Option 2</option></select>
6+
```
7+
8+
## Output
9+
10+
```js
11+
import {
12+
Fragment,
13+
render as $$render,
14+
createAstro as $$createAstro,
15+
createComponent as $$createComponent,
16+
renderComponent as $$renderComponent,
17+
renderHead as $$renderHead,
18+
maybeRenderHead as $$maybeRenderHead,
19+
unescapeHTML as $$unescapeHTML,
20+
renderSlot as $$renderSlot,
21+
mergeSlots as $$mergeSlots,
22+
addAttribute as $$addAttribute,
23+
spreadAttributes as $$spreadAttributes,
24+
defineStyleVars as $$defineStyleVars,
25+
defineScriptVars as $$defineScriptVars,
26+
renderTransition as $$renderTransition,
27+
createTransitionScope as $$createTransitionScope,
28+
renderScript as $$renderScript,
29+
createMetadata as $$createMetadata
30+
} from "http://localhost:3000/";
31+
32+
export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: [], hydrationDirectives: new Set([]), hoisted: [] });
33+
34+
const $$Component = $$createComponent(($$result, $$props, $$slots) => {
35+
36+
return $$render`${$$maybeRenderHead($$result)}<select><button><selectedcontent></button><option>Option 1</option><option>Option 2</option></select>`;
37+
}, undefined, undefined);
38+
export default $$Component;
39+
```
40+
---

internal/printer/print-to-js.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -820,19 +820,20 @@ func render1(p *printer, n *Node, opts RenderOptions) {
820820
// are those that can't have any contents.
821821
// nolint
822822
var voidElements = map[string]bool{
823-
"area": true,
824-
"base": true,
825-
"br": true,
826-
"col": true,
827-
"embed": true,
828-
"hr": true,
829-
"img": true,
830-
"input": true,
831-
"keygen": true, // "keygen" has been removed from the spec, but are kept here for backwards compatibility.
832-
"link": true,
833-
"meta": true,
834-
"param": true,
835-
"source": true,
836-
"track": true,
837-
"wbr": true,
823+
"area": true,
824+
"base": true,
825+
"br": true,
826+
"col": true,
827+
"embed": true,
828+
"hr": true,
829+
"img": true,
830+
"input": true,
831+
"keygen": true, // "keygen" has been removed from the spec, but are kept here for backwards compatibility.
832+
"link": true,
833+
"meta": true,
834+
"param": true,
835+
"selectedcontent": true,
836+
"source": true,
837+
"track": true,
838+
"wbr": true,
838839
}

internal/printer/printer_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,6 +1561,14 @@ const items = ["Dog", "Cat", "Platipus"];
15611561
name: "select in form",
15621562
source: `<form><select>{options.map((option) => (<option value={option.id}>{option.title}</option>))}</select><div><label>Title 3</label><input type="text" /></div><button type="submit">Submit</button></form>`,
15631563
},
1564+
{
1565+
name: "selectedcontent element in customizable select",
1566+
source: `<select><button><selectedcontent></selectedcontent></button><option>Option 1</option><option>Option 2</option></select>`,
1567+
},
1568+
{
1569+
name: "selectedcontent self-closing element",
1570+
source: `<select><button><selectedcontent /></button><option>Option 1</option><option>Option 2</option></select>`,
1571+
},
15641572
{
15651573
name: "Expression in form followed by other sibling forms",
15661574
source: "<form><p>No expression here. So the next form will render.</p></form><form><h3>{data.formLabelA}</h3></form><form><h3>{data.formLabelB}</h3></form><form><p>No expression here, but the last form before me had an expression, so my form didn't render.</p></form><form><h3>{data.formLabelC}</h3></form><div><p>Here is some in-between content</p></div><form><h3>{data.formLabelD}</h3></form>",

internal/token.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1211,12 +1211,18 @@ func (z *Tokenizer) readStartTag() TokenType {
12111211
}
12121212

12131213
// HTML void tags list: https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#syntax-elements
1214-
// Also look for a self-closing token thats not in the list (e.g. "<svg><path/></svg>")
1214+
// Also look for a self-closing token that's not in the list (e.g. "<svg><path/></svg>")
12151215
if z.startTagIn("area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr") || z.err == nil && z.buf[z.raw.End-2] == '/' {
12161216
// Reset tokenizer state for self-closing elements
12171217
z.rawTag = ""
12181218
return SelfClosingTagToken
12191219
}
1220+
// Special handling for selectedcontent - it's void but can have a closing tag in HTML
1221+
if z.startTagIn("selectedcontent") && z.err == nil && z.buf[z.raw.End-2] == '/' {
1222+
// Only treat as self-closing if it actually has />
1223+
z.rawTag = ""
1224+
return SelfClosingTagToken
1225+
}
12201226

12211227
// Handle TypeScript Generics
12221228
if len(z.expressionElementStack) > 0 && len(z.expressionElementStack[len(z.expressionElementStack)-1]) == 0 {

internal/token_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,16 @@ func TestBasic(t *testing.T) {
415415
`<select>{[1, 2, 3].map(num => <option>{num}</option>)}</select><div>Hello</div>`,
416416
[]TokenType{StartTagToken, StartExpressionToken, TextToken, StartTagToken, StartExpressionToken, TextToken, EndExpressionToken, EndTagToken, TextToken, EndExpressionToken, EndTagToken, StartTagToken, TextToken, EndTagToken},
417417
},
418+
{
419+
"selectedcontent element",
420+
`<select><button><selectedcontent></selectedcontent></button><option>A</option></select>`,
421+
[]TokenType{StartTagToken, StartTagToken, StartTagToken, EndTagToken, EndTagToken, StartTagToken, TextToken, EndTagToken, EndTagToken},
422+
},
423+
{
424+
"selectedcontent self-closing",
425+
`<select><button><selectedcontent /></button><option>A</option></select>`,
426+
[]TokenType{StartTagToken, StartTagToken, SelfClosingTagToken, EndTagToken, StartTagToken, TextToken, EndTagToken, EndTagToken},
427+
},
418428
{
419429
"single open brace",
420430
"<main id={`{`}></main>",

0 commit comments

Comments
 (0)