mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(oxfmt): support html-in-js templates closing a dynamic tag name (#28389)
Since HTML formatter supports `htm`'s `<//>` (special closing tag), we
should also support other cases like:
```js
html`
<${For} each=${() => items}>
<li class="item">${() => content}</li>
<li class="item">more items</li>
</${For}>
`;
```
This commit is contained in:
1 parent
0a42af6086
commit
336b2c1c11
7 files changed
+184
-17
No files matched your search
@@ -415,3 +415,28 @@ one on the line of an element before it (`<pre><b>x</b>${`) indents from that li
|
||||
Prettier 3.9.6 aligned an expression starting its line so; since 3.9.7 (prettier/prettier#19725, for idempotency) it indents it by the elements around the content instead,
|
||||
which leaves `})` right of the `${` it closes.
|
||||
Reformatting the expression changes no content: its source is code, not text in the element.
|
||||
|
||||
## dynamic-tag-name-end-tag
|
||||
|
||||
- Why: uniform-rule (same construct, same output: an element htm's `<//>` closes, which Prettier formats; prettier/prettier#17849)
|
||||
- Pin: `conformance/fixtures/edge-cases/html-in-js/dynamic-tag-name.js`
|
||||
- Conformance: `externals/webawesome/button/button.ts`
|
||||
|
||||
```js
|
||||
// input
|
||||
html`<${For} each=${() => items}><li class="item">${() => content}</li><li class="item">more items</li></${For}>`;
|
||||
|
||||
// ours
|
||||
html`<${For} each=${() => items}
|
||||
><li class="item">${() => content}</li>
|
||||
<li class="item">more items</li></${For}
|
||||
>`;
|
||||
|
||||
// prettier
|
||||
html`<${For} each=${() => items}><li class="item">${() => content}</li><li class="item">more items</li></${For}>`;
|
||||
```
|
||||
|
||||
An element whose tag name is a `${}` expression (lit's static `html`, Solid, htm) closes at an end tag named by one, as at htm's `<//>`, and is formatted the same.
|
||||
Prettier gives each `${}` its own placeholder, so the end tag names another element than the start tag: its parser stops ("Unexpected closing tag") and the template stays as written.
|
||||
The parser is told the placeholders, not the expressions (`oxc-htmlish-parser`'s `ParserOptions::template_placeholder`):
|
||||
an end tag named by one closes the innermost element named by one, and such an element written `/>` closes there.
|
||||
@@ -0,0 +1,2 @@
|
||||
html`<${For} each=${() => items}><li class="item">${() => content}</li><li class="item">more items</li></${For}>`;
|
||||
html`<${Layout}><${Header} title="a long title attribute" /><main class="content">main content</main><//>`;
|
||||
@@ -74,12 +74,14 @@
|
||||
|
||||
## html-in-js
|
||||
|
||||
### Option 1: 173/201 (86.07%)
|
||||
### Option 1: 173/202 (85.64%)
|
||||
|
||||
```json
|
||||
{"printWidth":80}
|
||||
```
|
||||
|
||||
- [edge-cases/html-in-js/dynamic-tag-name.js](diffs/html-in-js/edge-cases__html-in-js__dynamic-tag-name.js.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#dynamic-tag-name-end-tag
|
||||
- [edge-cases/html-in-js/placeholder-case-in-child.js](diffs/html-in-js/edge-cases__html-in-js__placeholder-case-in-child.js.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#placeholder-case-in-child
|
||||
- [edge-cases/html-in-js/pre-like-template-expression.js](diffs/html-in-js/edge-cases__html-in-js__pre-like-template-expression.js.md)
|
||||
@@ -91,7 +93,7 @@
|
||||
- [externals/webawesome/badge/badge.ts](diffs/html-in-js/externals__webawesome__badge__badge.ts.md)
|
||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
||||
- [externals/webawesome/button/button.ts](diffs/html-in-js/externals__webawesome__button__button.ts.md)
|
||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry, apps/oxfmt/DIVERGENCES.md#dynamic-tag-name-end-tag
|
||||
- [externals/webawesome/callout/callout.ts](diffs/html-in-js/externals__webawesome__callout__callout.ts.md)
|
||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
||||
- [externals/webawesome/checkbox/checkbox.ts](diffs/html-in-js/externals__webawesome__checkbox__checkbox.ts.md)
|
||||
@@ -137,12 +139,14 @@
|
||||
- [externals/webawesome/textarea/textarea.ts](diffs/html-in-js/externals__webawesome__textarea__textarea.ts.md)
|
||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
||||
|
||||
### Option 2: 187/201 (93.03%)
|
||||
### Option 2: 187/202 (92.57%)
|
||||
|
||||
```json
|
||||
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore"}
|
||||
```
|
||||
|
||||
- [edge-cases/html-in-js/dynamic-tag-name.js](diffs/html-in-js/edge-cases__html-in-js__dynamic-tag-name.js.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#dynamic-tag-name-end-tag
|
||||
- [edge-cases/html-in-js/placeholder-case-in-child.js](diffs/html-in-js/edge-cases__html-in-js__placeholder-case-in-child.js.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#placeholder-case-in-child
|
||||
- [edge-cases/html-in-js/pre-like-template-expression.js](diffs/html-in-js/edge-cases__html-in-js__pre-like-template-expression.js.md)
|
||||
@@ -152,7 +156,7 @@
|
||||
- [externals/prettier/js/embeded/indention/19518.js](diffs/html-in-js/externals__prettier__js__embeded__indention__19518.js.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#pre-like-template-expression
|
||||
- [externals/webawesome/button/button.ts](diffs/html-in-js/externals__webawesome__button__button.ts.md)
|
||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry, apps/oxfmt/DIVERGENCES.md#dynamic-tag-name-end-tag
|
||||
- [externals/webawesome/callout/callout.ts](diffs/html-in-js/externals__webawesome__callout__callout.ts.md)
|
||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
||||
- [externals/webawesome/copy-button/copy-button.ts](diffs/html-in-js/externals__webawesome__copy-button__copy-button.ts.md)
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# edge-cases/html-in-js/dynamic-tag-name.js
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,5 +1,8 @@
|
||||
-html`<${For} each=${() => items}><li class="item">${() => content}</li><li class="item">more items</li></${For}>`;
|
||||
+html`<${For} each=${() => items}
|
||||
+ ><li class="item">${() => content}</li>
|
||||
+ <li class="item">more items</li></${For}
|
||||
+>`;
|
||||
html`<${Layout}
|
||||
><${Header} title="a long title attribute" />
|
||||
<main class="content">main content</main><//
|
||||
>`;
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````js
|
||||
html`<${For} each=${() => items}
|
||||
><li class="item">${() => content}</li>
|
||||
<li class="item">more items</li></${For}
|
||||
>`;
|
||||
html`<${Layout}
|
||||
><${Header} title="a long title attribute" />
|
||||
<main class="content">main content</main><//
|
||||
>`;
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````js
|
||||
html`<${For} each=${() => items}><li class="item">${() => content}</li><li class="item">more items</li></${For}>`;
|
||||
html`<${Layout}
|
||||
><${Header} title="a long title attribute" />
|
||||
<main class="content">main content</main><//
|
||||
>`;
|
||||
|
||||
`````
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,6 +1,11 @@
|
||||
-html`<${For} each=${() => items}><li class="item">${() => content}</li><li class="item">more items</li></${For}>`;
|
||||
html`
|
||||
+ <${For} each=${() => items}>
|
||||
+ <li class="item">${() => content}</li>
|
||||
+ <li class="item">more items</li>
|
||||
+ </${For}>
|
||||
+`;
|
||||
+html`
|
||||
<${Layout}>
|
||||
<${Header} title="a long title attribute" />
|
||||
<main class="content">main content</main>
|
||||
<//>
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````js
|
||||
html`
|
||||
<${For} each=${() => items}>
|
||||
<li class="item">${() => content}</li>
|
||||
<li class="item">more items</li>
|
||||
</${For}>
|
||||
`;
|
||||
html`
|
||||
<${Layout}>
|
||||
<${Header} title="a long title attribute" />
|
||||
<main class="content">main content</main>
|
||||
<//>
|
||||
`;
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````js
|
||||
html`<${For} each=${() => items}><li class="item">${() => content}</li><li class="item">more items</li></${For}>`;
|
||||
html`
|
||||
<${Layout}>
|
||||
<${Header} title="a long title attribute" />
|
||||
<main class="content">main content</main>
|
||||
<//>
|
||||
`;
|
||||
|
||||
`````
|
||||
+4
-4
@@ -15,7 +15,7 @@
|
||||
@@ -1,7 +1,7 @@
|
||||
const a = html`<style>
|
||||
a {
|
||||
- prettier_html_placeholder_0_13_in_js: ${c};
|
||||
- prettier_html_placeholder_0_15_in_js: ${c};
|
||||
+ ${b}: ${c};
|
||||
}
|
||||
</style>`;
|
||||
@@ -46,7 +46,7 @@ const d = html`<style>
|
||||
`````js
|
||||
const a = html`<style>
|
||||
a {
|
||||
prettier_html_placeholder_0_13_in_js: ${c};
|
||||
prettier_html_placeholder_0_15_in_js: ${c};
|
||||
}
|
||||
</style>`;
|
||||
const d = html`<style>
|
||||
@@ -74,7 +74,7 @@ const d = html`<style>
|
||||
const a = html`
|
||||
<style>
|
||||
a {
|
||||
- prettier_html_placeholder_0_2992_in_js: ${c};
|
||||
- prettier_html_placeholder_0_2996_in_js: ${c};
|
||||
+ ${b}: ${c};
|
||||
}
|
||||
</style>
|
||||
@@ -110,7 +110,7 @@ const d = html`
|
||||
const a = html`
|
||||
<style>
|
||||
a {
|
||||
prettier_html_placeholder_0_2992_in_js: ${c};
|
||||
prettier_html_placeholder_0_2996_in_js: ${c};
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
+20
-1
@@ -76,6 +76,21 @@
|
||||
private constructLightDOMButton() {
|
||||
const button = document.createElement("button");
|
||||
|
||||
@@ -351,9 +372,13 @@
|
||||
@invalid=${this.isButton() ? this.handleInvalid : null}
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
<slot name="start" part="start" class="start"></slot>
|
||||
- <slot part="label" class="label" @slotchange=${this.handleLabelSlotChange}></slot>
|
||||
+ <slot
|
||||
+ part="label"
|
||||
+ class="label"
|
||||
+ @slotchange=${this.handleLabelSlotChange}
|
||||
+ ></slot>
|
||||
<slot name="end" part="end" class="end"></slot>
|
||||
${
|
||||
this.withCaret
|
||||
? html`
|
||||
|
||||
`````
|
||||
|
||||
@@ -457,7 +472,11 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
<slot name="start" part="start" class="start"></slot>
|
||||
<slot part="label" class="label" @slotchange=${this.handleLabelSlotChange}></slot>
|
||||
<slot
|
||||
part="label"
|
||||
class="label"
|
||||
@slotchange=${this.handleLabelSlotChange}
|
||||
></slot>
|
||||
<slot name="end" part="end" class="end"></slot>
|
||||
${
|
||||
this.withCaret
|
||||
|
||||
@@ -7,7 +7,7 @@ use oxc_formatter_core::{
|
||||
write, write_front_matter,
|
||||
};
|
||||
use oxc_htmlish_parser::{
|
||||
Dialect, Parser,
|
||||
Dialect, Parser, ParserOptions,
|
||||
ast::{NodeId, Root},
|
||||
};
|
||||
|
||||
@@ -95,10 +95,11 @@ pub fn parse_for_format<'a>(
|
||||
options: &HtmlishFormatOptions,
|
||||
) -> Result<ParsedHtmlish<'a>, OxcDiagnostic> {
|
||||
let (has_bom, stripped) = split_bom(source_text);
|
||||
parse_root(allocator, stripped, has_bom, options.variant.to_dialect()).map_err(|mut err| {
|
||||
oxc_formatter_core::rebase_labels(&mut err.labels, source_text, stripped);
|
||||
err
|
||||
})
|
||||
parse_root(allocator, stripped, has_bom, options.variant.to_dialect(), ParserOptions::default())
|
||||
.map_err(|mut err| {
|
||||
oxc_formatter_core::rebase_labels(&mut err.labels, source_text, stripped);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse `source_text` and build the formatter IR for embedding into another
|
||||
@@ -117,8 +118,11 @@ pub fn format_to_ir<'a>(
|
||||
embedded_in: Option<HtmlishEmbeddedIn>,
|
||||
) -> Result<EmbeddedIr<'a>, OxcDiagnostic> {
|
||||
let allocator = session.allocator();
|
||||
let template_placeholder = embedded_in.and_then(|e| e.placeholder).map(|(_, is)| is);
|
||||
let parser_options = ParserOptions { template_placeholder };
|
||||
// `FormatSession::dispatch` never hands a BOM-headed input to an embedded part
|
||||
let parsed = parse_root(allocator, source_text, false, options.variant.to_dialect())?;
|
||||
let parsed =
|
||||
parse_root(allocator, source_text, false, options.variant.to_dialect(), parser_options)?;
|
||||
if parsed.front_matter.is_some() && !session.input_kind().owns_front_matter() {
|
||||
// A fragment never acquires file envelope semantics
|
||||
return Err(OxcDiagnostic::error(
|
||||
@@ -147,7 +151,8 @@ pub struct HtmlishEmbeddedIn {
|
||||
pub indent_multiple_roots: bool,
|
||||
/// The parent's placeholder for an expression (html-in-js's `${}`):
|
||||
/// a value that is one stays unquoted (`attr=${value}` passes a value, not a string),
|
||||
/// and a value holding one is no class list to sort.
|
||||
/// a value holding one is no class list to sort,
|
||||
/// and a tag named by one is a component (`<${Tag} />`, `<${Tag}>…</${Tag}>`).
|
||||
pub placeholder: Option<Placeholder>,
|
||||
}
|
||||
|
||||
@@ -175,6 +180,7 @@ fn parse_root<'a>(
|
||||
source_text: &str,
|
||||
has_bom: bool,
|
||||
dialect: Dialect,
|
||||
options: ParserOptions,
|
||||
) -> Result<ParsedHtmlish<'a>, OxcDiagnostic> {
|
||||
// The printer slices verbatim text from the source, and the IR forbids `\r`.
|
||||
let unblanked: &'a str =
|
||||
@@ -185,7 +191,8 @@ fn parse_root<'a>(
|
||||
Some(fm) => allocator.alloc_str(&blank_front_matter(unblanked, fm.raw.len())),
|
||||
None => unblanked,
|
||||
};
|
||||
let root = Parser::new(allocator, source, dialect).parse().map_err(|diagnostic| {
|
||||
let root = Parser::new(allocator, source, dialect).with_options(options).parse();
|
||||
let root = root.map_err(|diagnostic| {
|
||||
let mut err = OxcDiagnostic::error(format!("Syntax error: {diagnostic}"))
|
||||
.with_label(oxc_span::Span::new(diagnostic.span.start, diagnostic.span.end));
|
||||
oxc_formatter_core::restore_crlf_labels(&mut err.labels, source_text);
|
||||
|
||||
Reference in new issue
Block a user