From 858da76e4b63936bcc17e31ca14f3b07a4c4b525 Mon Sep 17 00:00:00 2001 From: jp-knj Date: Sun, 31 Aug 2025 12:32:49 +0900 Subject: [PATCH 1/2] 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 --- internal/parser.go | 27 +++++++++++++ ...ontent_element_in_customizable_select.snap | 40 +++++++++++++++++++ .../selectedcontent_self-closing_element.snap | 40 +++++++++++++++++++ internal/printer/print-to-js.go | 31 +++++++------- internal/printer/printer_test.go | 8 ++++ internal/token.go | 8 +++- internal/token_test.go | 10 +++++ 7 files changed, 148 insertions(+), 16 deletions(-) create mode 100755 internal/printer/__printer_js__/selectedcontent_element_in_customizable_select.snap create mode 100755 internal/printer/__printer_js__/selectedcontent_self-closing_element.snap diff --git a/internal/parser.go b/internal/parser.go index 42d7eb0d8..9cdf4c2df 100644 --- a/internal/parser.go +++ b/internal/parser.go @@ -1418,6 +1418,14 @@ func inBodyIM(p *parser) bool { return true } default: + // Special handling for selectedcontent as a void element + if p.tok.Data == "selectedcontent" { + p.reconstructActiveFormattingElements() + p.addElement() + p.oe.pop() + p.acknowledgeSelfClosingTag() + return true + } p.reconstructActiveFormattingElements() p.addElement() if p.hasSelfClosingToken { @@ -1436,6 +1444,12 @@ func inBodyIM(p *parser) bool { return true } + // Special handling for selectedcontent end tag - just ignore it + // since it's treated as a void element + if p.tok.Data == "selectedcontent" { + return true + } + switch p.tok.DataAtom { case a.Body: p.addLoc() @@ -2319,6 +2333,19 @@ func inSelectIM(p *parser) bool { p.resetInsertionMode() case a.Template: return inHeadIM(p) + default: + // Handle closing tags for elements that are allowed in customizable select + // (like button for the new HTML select element) + if p.tok.Data == "button" { + // Close the button if it's open + for i := len(p.oe) - 1; i >= 0; i-- { + if p.oe[i].Data == "button" { + p.oe = p.oe[:i] + break + } + } + return true + } } case CommentToken: p.addChild(&Node{ diff --git a/internal/printer/__printer_js__/selectedcontent_element_in_customizable_select.snap b/internal/printer/__printer_js__/selectedcontent_element_in_customizable_select.snap new file mode 100755 index 000000000..cf64eb0ed --- /dev/null +++ b/internal/printer/__printer_js__/selectedcontent_element_in_customizable_select.snap @@ -0,0 +1,40 @@ +[TestPrinter/selectedcontent_element_in_customizable_select - 1] +## Input + +``` + +``` + +## Output + +```js +import { + Fragment, + render as $$render, + createAstro as $$createAstro, + createComponent as $$createComponent, + renderComponent as $$renderComponent, + renderHead as $$renderHead, + maybeRenderHead as $$maybeRenderHead, + unescapeHTML as $$unescapeHTML, + renderSlot as $$renderSlot, + mergeSlots as $$mergeSlots, + addAttribute as $$addAttribute, + spreadAttributes as $$spreadAttributes, + defineStyleVars as $$defineStyleVars, + defineScriptVars as $$defineScriptVars, + renderTransition as $$renderTransition, + createTransitionScope as $$createTransitionScope, + renderScript as $$renderScript, + createMetadata as $$createMetadata +} from "http://localhost:3000/"; + +export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: [], hydrationDirectives: new Set([]), hoisted: [] }); + +const $$Component = $$createComponent(($$result, $$props, $$slots) => { + +return $$render`${$$maybeRenderHead($$result)}`; +}, undefined, undefined); +export default $$Component; +``` +--- diff --git a/internal/printer/__printer_js__/selectedcontent_self-closing_element.snap b/internal/printer/__printer_js__/selectedcontent_self-closing_element.snap new file mode 100755 index 000000000..767835155 --- /dev/null +++ b/internal/printer/__printer_js__/selectedcontent_self-closing_element.snap @@ -0,0 +1,40 @@ +[TestPrinter/selectedcontent_self-closing_element - 1] +## Input + +``` + +``` + +## Output + +```js +import { + Fragment, + render as $$render, + createAstro as $$createAstro, + createComponent as $$createComponent, + renderComponent as $$renderComponent, + renderHead as $$renderHead, + maybeRenderHead as $$maybeRenderHead, + unescapeHTML as $$unescapeHTML, + renderSlot as $$renderSlot, + mergeSlots as $$mergeSlots, + addAttribute as $$addAttribute, + spreadAttributes as $$spreadAttributes, + defineStyleVars as $$defineStyleVars, + defineScriptVars as $$defineScriptVars, + renderTransition as $$renderTransition, + createTransitionScope as $$createTransitionScope, + renderScript as $$renderScript, + createMetadata as $$createMetadata +} from "http://localhost:3000/"; + +export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: [], hydrationDirectives: new Set([]), hoisted: [] }); + +const $$Component = $$createComponent(($$result, $$props, $$slots) => { + +return $$render`${$$maybeRenderHead($$result)}`; +}, undefined, undefined); +export default $$Component; +``` +--- diff --git a/internal/printer/print-to-js.go b/internal/printer/print-to-js.go index 2e8bda524..2013168f4 100644 --- a/internal/printer/print-to-js.go +++ b/internal/printer/print-to-js.go @@ -820,19 +820,20 @@ func render1(p *printer, n *Node, opts RenderOptions) { // are those that can't have any contents. // nolint var voidElements = map[string]bool{ - "area": true, - "base": true, - "br": true, - "col": true, - "embed": true, - "hr": true, - "img": true, - "input": true, - "keygen": true, // "keygen" has been removed from the spec, but are kept here for backwards compatibility. - "link": true, - "meta": true, - "param": true, - "source": true, - "track": true, - "wbr": true, + "area": true, + "base": true, + "br": true, + "col": true, + "embed": true, + "hr": true, + "img": true, + "input": true, + "keygen": true, // "keygen" has been removed from the spec, but are kept here for backwards compatibility. + "link": true, + "meta": true, + "param": true, + "selectedcontent": true, + "source": true, + "track": true, + "wbr": true, } diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index fedea94a3..11b23cb0a 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -1561,6 +1561,14 @@ const items = ["Dog", "Cat", "Platipus"]; name: "select in form", source: `
`, }, + { + name: "selectedcontent element in customizable select", + source: ``, + }, + { + name: "selectedcontent self-closing element", + source: ``, + }, { name: "Expression in form followed by other sibling forms", source: "

No expression here. So the next form will render.

{data.formLabelA}

{data.formLabelB}

No expression here, but the last form before me had an expression, so my form didn't render.

{data.formLabelC}

Here is some in-between content

{data.formLabelD}

", diff --git a/internal/token.go b/internal/token.go index ef2ea3fa2..480d82146 100644 --- a/internal/token.go +++ b/internal/token.go @@ -1211,12 +1211,18 @@ func (z *Tokenizer) readStartTag() TokenType { } // HTML void tags list: https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#syntax-elements - // Also look for a self-closing token that’s not in the list (e.g. "") + // Also look for a self-closing token that's not in the list (e.g. "") 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] == '/' { // Reset tokenizer state for self-closing elements z.rawTag = "" return SelfClosingTagToken } + // Special handling for selectedcontent - it's void but can have a closing tag in HTML + if z.startTagIn("selectedcontent") && z.err == nil && z.buf[z.raw.End-2] == '/' { + // Only treat as self-closing if it actually has /> + z.rawTag = "" + return SelfClosingTagToken + } // Handle TypeScript Generics if len(z.expressionElementStack) > 0 && len(z.expressionElementStack[len(z.expressionElementStack)-1]) == 0 { diff --git a/internal/token_test.go b/internal/token_test.go index 39992f6b9..cb3542d96 100644 --- a/internal/token_test.go +++ b/internal/token_test.go @@ -415,6 +415,16 @@ func TestBasic(t *testing.T) { `
Hello
`, []TokenType{StartTagToken, StartExpressionToken, TextToken, StartTagToken, StartExpressionToken, TextToken, EndExpressionToken, EndTagToken, TextToken, EndExpressionToken, EndTagToken, StartTagToken, TextToken, EndTagToken}, }, + { + "selectedcontent element", + ``, + []TokenType{StartTagToken, StartTagToken, StartTagToken, EndTagToken, EndTagToken, StartTagToken, TextToken, EndTagToken, EndTagToken}, + }, + { + "selectedcontent self-closing", + ``, + []TokenType{StartTagToken, StartTagToken, SelfClosingTagToken, EndTagToken, StartTagToken, TextToken, EndTagToken, EndTagToken}, + }, { "single open brace", "
", From 1ee2a8801a19fe011e7d91b62e173561818704b5 Mon Sep 17 00:00:00 2001 From: jp-knj Date: Mon, 8 Sep 2025 01:40:00 +0900 Subject: [PATCH 2/2] chore: add changeset --- .changeset/happy-ways-collect.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/happy-ways-collect.md diff --git a/.changeset/happy-ways-collect.md b/.changeset/happy-ways-collect.md new file mode 100644 index 000000000..78abad3d2 --- /dev/null +++ b/.changeset/happy-ways-collect.md @@ -0,0 +1,8 @@ +--- +"@astrojs/compiler": minor +--- + +Support HTML element + +Based on the recent commit history, this change appears to be related to fixing issue #1093 regarding selectedcontent parsing in customizable selects. The element is part of the new Customizable Select Element API +in HTML, used within elements to display the currently selected option(s). \ No newline at end of file