From bbbe303db76056a682fc5ca279f1d428b90bf15f Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 21 Nov 2025 11:05:28 +0800 Subject: [PATCH 01/67] fix: remove unexpected spaces in Mustache interpolation --- markup_fmt/src/printer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 23311157..c7de2e88 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -732,7 +732,7 @@ impl<'s> DocGen<'s> for Element<'s> { "{{{{ {} }}}}", ctx.format_expr(expr, false, *start), )), - Language::Mustache => Cow::from(format!("{{{{ {expr} }}}}")), + Language::Mustache => Cow::from(format!("{{{{{expr}}}}}")), _ => unreachable!(), })) .collect::() @@ -1132,7 +1132,7 @@ impl<'s> DocGen<'s> for NativeAttribute<'s> { "{{{{ {} }}}}", ctx.format_expr(expr, true, *start), )), - Language::Mustache => Cow::from(format!("{{{{ {expr} }}}}")), + Language::Mustache => Cow::from(format!("{{{{{expr}}}}}")), _ => unreachable!(), })) .collect::(), From 5eae751874a0865518b527a38bf874d66843d166 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Sat, 22 Nov 2025 11:59:02 +0800 Subject: [PATCH 02/67] feat: support Handlebars (close #128) --- .gitattributes | 1 + .vscode/settings.json | 2 +- README.md | 4 +- dprint_plugin/src/config.rs | 2 + markup_fmt/Cargo.toml | 2 +- markup_fmt/README.md | 2 +- markup_fmt/src/ast.rs | 12 ++- markup_fmt/src/lib.rs | 2 +- markup_fmt/src/parser.rs | 78 ++++++++++++++++--- markup_fmt/src/printer.rs | 49 +++++++----- markup_fmt/tests/fmt.rs | 2 +- .../tests/fmt/mustache/handlebars/block.hbs | 13 ++++ .../tests/fmt/mustache/handlebars/block.snap | 16 ++++ .../tests/fmt/mustache/handlebars/context.hbs | 9 +++ .../fmt/mustache/handlebars/context.snap | 12 +++ .../tests/fmt/mustache/handlebars/partial.hbs | 8 ++ .../fmt/mustache/handlebars/partial.snap | 11 +++ 17 files changed, 184 insertions(+), 41 deletions(-) create mode 100644 markup_fmt/tests/fmt/mustache/handlebars/block.hbs create mode 100644 markup_fmt/tests/fmt/mustache/handlebars/block.snap create mode 100644 markup_fmt/tests/fmt/mustache/handlebars/context.hbs create mode 100644 markup_fmt/tests/fmt/mustache/handlebars/context.snap create mode 100644 markup_fmt/tests/fmt/mustache/handlebars/partial.hbs create mode 100644 markup_fmt/tests/fmt/mustache/handlebars/partial.snap diff --git a/.gitattributes b/.gitattributes index 6c0983bc..d9518406 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,4 @@ **/*.njk linguist-detectable=false **/*.vto linguist-detectable=false **/*.mustache linguist-detectable=false +**/*.hbs linguist-detectable=false diff --git a/.vscode/settings.json b/.vscode/settings.json index 1c59ca82..b6055b31 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "rust-analyzer.cargo.features": ["config_serde"], - "[html][vue][svelte][markdown]": { + "[html][vue][svelte][markdown][handlebars]": { "editor.formatOnSave": false } } diff --git a/README.md b/README.md index 40c91976..42326641 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

markup_fmt

-markup_fmt is a configurable HTML, Vue, Svelte, Astro, Angular, Jinja, Twig, Nunjucks, Vento, Mustache and XML formatter. +markup_fmt is a configurable HTML, Vue, Svelte, Astro, Angular, Jinja, Twig, Nunjucks, Vento, Mustache, Handlebars and XML formatter.

## Notes for Vue and Svelte Users @@ -20,7 +20,7 @@ This will make ESLint faster because less rules will be executed. We've provided [dprint](https://dprint.dev/) integration. -This plugin only formats HTML syntax of your HTML, Vue, Svelte, Astro, Angular, Jinja, Twig, Nunjucks, Vento, Mustache and XML files. +This plugin only formats HTML syntax of your HTML, Vue, Svelte, Astro, Angular, Jinja, Twig, Nunjucks, Vento, Mustache, Handlebars and XML files. You also need other dprint plugins to format the code in ` + diff --git a/dprint_plugin/tests/integration/json.html b/dprint_plugin/tests/integration/json.html index 6705b91f..0863f21e 100644 --- a/dprint_plugin/tests/integration/json.html +++ b/dprint_plugin/tests/integration/json.html @@ -3,6 +3,9 @@ + diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 58972434..6bfadb58 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -673,9 +673,13 @@ impl<'s> DocGen<'s> for Element<'s> { if let Attribute::Native(native_attr) = attr { native_attr.name.eq_ignore_ascii_case("type") && native_attr.value.is_some_and(|(value, _)| { - value == "importmap" - || value == "application/json" - || value == "application/ld+json" + matches!( + value, + "importmap" + | "application/json" + | "application/ld+json" + | "speculationrules" + ) }) } else { false From abb899131d2974ad64a7ab2e4c890c7f5638e71e Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Sun, 28 Dec 2025 11:02:16 +0800 Subject: [PATCH 17/67] fix: fix Vento interpolation trimming (fix #181) --- markup_fmt/src/ast.rs | 2 ++ markup_fmt/src/parser.rs | 2 ++ markup_fmt/src/printer.rs | 10 ++++++++++ .../tests/fmt/vento/interpolation/interpolation.snap | 6 ++++++ .../tests/fmt/vento/interpolation/interpolation.vto | 3 +++ 5 files changed, 23 insertions(+) create mode 100644 markup_fmt/tests/fmt/vento/interpolation/interpolation.snap create mode 100644 markup_fmt/tests/fmt/vento/interpolation/interpolation.vto diff --git a/markup_fmt/src/ast.rs b/markup_fmt/src/ast.rs index 0a443b75..4b4c0f22 100644 --- a/markup_fmt/src/ast.rs +++ b/markup_fmt/src/ast.rs @@ -437,6 +437,8 @@ pub struct VentoEval<'s> { pub struct VentoInterpolation<'s> { pub expr: &'s str, pub start: usize, + pub trim_prev: bool, + pub trim_next: bool, } #[derive(Debug)] diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index dc83261f..15b0dfee 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -2834,6 +2834,8 @@ impl<'s> Parser<'s> { Ok(NodeKind::VentoInterpolation(VentoInterpolation { expr: first_tag, start: first_tag_start, + trim_prev, + trim_next, })) } else { Ok(NodeKind::VentoTag(VentoTag { diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 6bfadb58..093dda07 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -1732,6 +1732,11 @@ impl<'s> DocGen<'s> for VentoInterpolation<'s> { F: for<'a> FnMut(&'a str, Hints) -> Result, E>, { Doc::text("{{") + .append(if self.trim_prev { + Doc::text("-") + } else { + Doc::nil() + }) .append(Doc::line_or_space()) .concat(itertools::intersperse( self.expr.split("|>").map(|expr| { @@ -1746,6 +1751,11 @@ impl<'s> DocGen<'s> for VentoInterpolation<'s> { )) .nest(ctx.indent_width) .append(Doc::line_or_space()) + .append(if self.trim_next { + Doc::text("-") + } else { + Doc::nil() + }) .append(Doc::text("}}")) .group() } diff --git a/markup_fmt/tests/fmt/vento/interpolation/interpolation.snap b/markup_fmt/tests/fmt/vento/interpolation/interpolation.snap new file mode 100644 index 00000000..de45d5e0 --- /dev/null +++ b/markup_fmt/tests/fmt/vento/interpolation/interpolation.snap @@ -0,0 +1,6 @@ +--- +source: markup_fmt/tests/fmt.rs +--- +{{- example }} +{{ example -}} +{{- example -}} diff --git a/markup_fmt/tests/fmt/vento/interpolation/interpolation.vto b/markup_fmt/tests/fmt/vento/interpolation/interpolation.vto new file mode 100644 index 00000000..e9d78db2 --- /dev/null +++ b/markup_fmt/tests/fmt/vento/interpolation/interpolation.vto @@ -0,0 +1,3 @@ +{{- example }} +{{ example -}} +{{- example -}} From 17a3d0fb1e1c7e5ac9ed2af52e09e3b5cbbb0b97 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Wed, 31 Dec 2025 16:38:19 +0800 Subject: [PATCH 18/67] fix: treat unknown type of script tag as raw text (fix #179) --- markup_fmt/src/printer.rs | 144 ++++++++++-------- .../tests/fmt/html/whitespace/snippets.snap | 24 +-- 2 files changed, 94 insertions(+), 74 deletions(-) diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 093dda07..097eaa3b 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -669,76 +669,96 @@ impl<'s> DocGen<'s> for Element<'s> { if text_node.raw.chars().all(|c| c.is_ascii_whitespace()) { docs.push(Doc::hard_line()); } else { - let is_json = self.attrs.iter().any(|attr| { - if let Attribute::Native(native_attr) = attr { - native_attr.name.eq_ignore_ascii_case("type") - && native_attr.value.is_some_and(|(value, _)| { - matches!( - value, - "importmap" - | "application/json" - | "application/ld+json" - | "speculationrules" - ) - }) - } else { - false + let type_attr = self.attrs.iter().find_map(|attr| match attr { + Attribute::Native(native) if native.name.eq_ignore_ascii_case("type") => { + native.value.map(|(value, _)| value.to_ascii_lowercase()) } + _ => None, }); - let is_script_indent = ctx.script_indent(); - let formatted = if is_json { - ctx.format_json(text_node.raw, text_node.start, &state) - } else { - if is_script_indent { - state.indent_level += 1; - } - let lang = self - .attrs - .iter() - .find_map(|attr| match attr { + match type_attr.as_deref() { + Some( + "module" + | "application/javascript" + | "text/javascript" + | "application/ecmascript" + | "text/ecmascript" + | "application/x-javascript" + | "application/x-ecmascript" + | "text/x-javascript" + | "text/x-ecmascript" + | "text/jsx" + | "text/babel", + ) + | None => { + let is_script_indent = ctx.script_indent(); + if is_script_indent { + state.indent_level += 1; + } + let lang = self + .attrs + .iter() + .find_map(|attr| match attr { + Attribute::Native(native) + if native.name.eq_ignore_ascii_case("lang") => + { + native.value.map(|(value, _)| value) + } + _ => None, + }) + .unwrap_or(if matches!(ctx.language, Language::Astro) { + "ts" + } else { + "js" + }); + let lang = if self.attrs.iter().any(|attr| match attr { Attribute::Native(native) - if native.name.eq_ignore_ascii_case("lang") => + if native.name.eq_ignore_ascii_case("type") => { - native.value.map(|(value, _)| value) + native.value.is_some_and(|(value, _)| value == "module") + } + _ => false, + }) { + match lang { + "ts" => "mts", + "js" => "mjs", + lang => lang, } - _ => None, - }) - .unwrap_or(if matches!(ctx.language, Language::Astro) { - "ts" } else { - "js" - }); - let lang = if self.attrs.iter().any(|attr| match attr { - Attribute::Native(native) if native.name.eq_ignore_ascii_case("type") => { - native.value.is_some_and(|(value, _)| value == "module") - } - _ => false, - }) { - match lang { - "ts" => "mts", - "js" => "mjs", - lang => lang, + lang + }; + let formatted = + ctx.format_script(text_node.raw, lang, text_node.start, &state); + let doc = if matches!( + ctx.options.script_formatter, + Some(ScriptFormatter::Dprint) + ) { + Doc::hard_line().concat(reflow_owned(formatted.trim())) + } else { + Doc::hard_line().concat(reflow_with_indent(formatted.trim(), true)) + }; + if is_script_indent { + docs.push(doc.nest(ctx.indent_width)); + } else { + docs.push(doc); } - } else { - lang - }; - ctx.format_script(text_node.raw, lang, text_node.start, &state) - }; - let doc = if !is_json - && matches!(ctx.options.script_formatter, Some(ScriptFormatter::Dprint)) - { - Doc::hard_line().concat(reflow_owned(formatted.trim())) - } else { - Doc::hard_line().concat(reflow_with_indent(formatted.trim(), true)) - }; - docs.push( - if is_script_indent { - doc.nest(ctx.indent_width) - } else { - doc } - .append(Doc::hard_line()), - ); + Some( + "importmap" + | "application/json" + | "application/ld+json" + | "speculationrules", + ) => { + let formatted = ctx.format_json(text_node.raw, text_node.start, &state); + docs.push( + Doc::hard_line().concat(reflow_with_indent(formatted.trim(), true)), + ); + } + Some(..) => { + docs.push(Doc::hard_line()); + docs.extend(reflow_raw(text_node.raw.trim_matches('\n'))); + } + } + docs.push(Doc::hard_line()); } } else if tag_name.eq_ignore_ascii_case("style") && let [ diff --git a/markup_fmt/tests/fmt/html/whitespace/snippets.snap b/markup_fmt/tests/fmt/html/whitespace/snippets.snap index fff5b575..0082315a 100644 --- a/markup_fmt/tests/fmt/html/whitespace/snippets.snap +++ b/markup_fmt/tests/fmt/html/whitespace/snippets.snap @@ -39,24 +39,24 @@ source: markup_fmt/tests/fmt.rs

X   or   Y

X   or   Y

From 8c30321d73ec0d587e755e4cabb86819807637e1 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 6 Jan 2026 09:36:28 +0800 Subject: [PATCH 19/67] fix: remove unexpected comma in JS expr (fix #183) --- markup_fmt/src/ctx.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/markup_fmt/src/ctx.rs b/markup_fmt/src/ctx.rs index ea2b8bfc..734a3783 100644 --- a/markup_fmt/src/ctx.rs +++ b/markup_fmt/src/ctx.rs @@ -143,7 +143,9 @@ where // This makes sure the code will be trimmed // though external formatter isn't available. let preprocessed = code.trim_start(); - let wrapped = if preprocessed.starts_with('{') || preprocessed.starts_with("...") { + let will_add_brackets = + preprocessed.starts_with('{') || preprocessed.starts_with("..."); + let wrapped = if will_add_brackets { self.source .get(0..start.saturating_sub(1)) .unwrap_or_default() @@ -171,6 +173,9 @@ where formatted.trim_matches(|c: char| c.is_ascii_whitespace() || c == ';'); formatted = trim_delim(preprocessed, formatted, '[', ']'); formatted = trim_delim(preprocessed, formatted, '(', ')'); + if will_add_brackets { + formatted = formatted.trim_ascii_end().trim_end_matches(','); + } Ok(formatted.trim_ascii().to_owned()) } } From 4e6c2b3f7586656fe9506824e7f810caf45cad4f Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 6 Jan 2026 11:22:21 +0800 Subject: [PATCH 20/67] fix: fix unterminated comment and interpolation (fix #184) --- markup_fmt/src/parser.rs | 14 ++++++++++---- .../fmt/jinja/invalid/unterminated-comment.jinja | 5 +++++ .../fmt/jinja/invalid/unterminated-comment.snap | 9 +++++++++ .../jinja/invalid/unterminated-interpolation.jinja | 5 +++++ .../jinja/invalid/unterminated-interpolation.snap | 10 ++++++++++ 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 markup_fmt/tests/fmt/jinja/invalid/unterminated-comment.jinja create mode 100644 markup_fmt/tests/fmt/jinja/invalid/unterminated-comment.snap create mode 100644 markup_fmt/tests/fmt/jinja/invalid/unterminated-interpolation.jinja create mode 100644 markup_fmt/tests/fmt/jinja/invalid/unterminated-interpolation.snap diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index 15b0dfee..b3ee5274 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -1379,7 +1379,7 @@ impl<'s> Parser<'s> { }; let start = start + 1; - let mut end = start; + let end; loop { match self.chars.next() { Some((i, '#')) => { @@ -1391,7 +1391,10 @@ impl<'s> Parser<'s> { } } Some(..) => continue, - None => break, + None => { + end = self.source.len(); + break; + } } } @@ -1598,7 +1601,7 @@ impl<'s> Parser<'s> { let start = start + 1; let mut braces_stack = 0usize; - let mut end = start; + let end; loop { match self.chars.next() { Some((_, '{')) => braces_stack += 1, @@ -1613,7 +1616,10 @@ impl<'s> Parser<'s> { } } Some(..) => continue, - None => break, + None => { + end = self.source.len(); + break; + } } } diff --git a/markup_fmt/tests/fmt/jinja/invalid/unterminated-comment.jinja b/markup_fmt/tests/fmt/jinja/invalid/unterminated-comment.jinja new file mode 100644 index 00000000..a91ceb60 --- /dev/null +++ b/markup_fmt/tests/fmt/jinja/invalid/unterminated-comment.jinja @@ -0,0 +1,5 @@ +{# comment %} + +This gets nuked + +This too diff --git a/markup_fmt/tests/fmt/jinja/invalid/unterminated-comment.snap b/markup_fmt/tests/fmt/jinja/invalid/unterminated-comment.snap new file mode 100644 index 00000000..a9b73581 --- /dev/null +++ b/markup_fmt/tests/fmt/jinja/invalid/unterminated-comment.snap @@ -0,0 +1,9 @@ +--- +source: markup_fmt/tests/fmt.rs +--- +{# comment %} + +This gets nuked + +This too +#} diff --git a/markup_fmt/tests/fmt/jinja/invalid/unterminated-interpolation.jinja b/markup_fmt/tests/fmt/jinja/invalid/unterminated-interpolation.jinja new file mode 100644 index 00000000..b1ab4423 --- /dev/null +++ b/markup_fmt/tests/fmt/jinja/invalid/unterminated-interpolation.jinja @@ -0,0 +1,5 @@ +{{ comment #} + +This gets nuked + +This too diff --git a/markup_fmt/tests/fmt/jinja/invalid/unterminated-interpolation.snap b/markup_fmt/tests/fmt/jinja/invalid/unterminated-interpolation.snap new file mode 100644 index 00000000..687d3b1d --- /dev/null +++ b/markup_fmt/tests/fmt/jinja/invalid/unterminated-interpolation.snap @@ -0,0 +1,10 @@ +--- +source: markup_fmt/tests/fmt.rs +--- +{{ + comment #} + + This gets nuked + + This too +}} From e5b8fa7ff33f4d6aa9ebab2194738f7c115759e6 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Sun, 11 Jan 2026 09:31:07 +0800 Subject: [PATCH 21/67] fix: fix Jinja interpolation trimming (fix #187) --- markup_fmt/src/ast.rs | 2 ++ markup_fmt/src/parser.rs | 16 +++++++++++++++- markup_fmt/src/printer.rs | 10 ++++++++++ .../tests/fmt/jinja/interpolation/fixture.jinja | 4 ++++ .../tests/fmt/jinja/interpolation/fixture.snap | 4 ++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/markup_fmt/src/ast.rs b/markup_fmt/src/ast.rs index 4b4c0f22..b5764216 100644 --- a/markup_fmt/src/ast.rs +++ b/markup_fmt/src/ast.rs @@ -186,6 +186,8 @@ pub struct JinjaComment<'s> { pub struct JinjaInterpolation<'s> { pub expr: &'s str, pub start: usize, + pub trim_prev: bool, + pub trim_next: bool, } #[derive(Debug)] diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index b3ee5274..8e53c4c1 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -1705,9 +1705,23 @@ impl<'s> Parser<'s> { NodeKind::VueInterpolation(VueInterpolation { expr, start }) } Language::Jinja => { + let (trim_prev, expr) = + if let Some(rest) = expr.strip_prefix('-') { + (true, rest) + } else { + (false, expr) + }; + let (trim_next, expr) = + if let Some(rest) = expr.strip_suffix('-') { + (true, rest) + } else { + (false, expr) + }; NodeKind::JinjaInterpolation(JinjaInterpolation { expr, - start, + start: if trim_prev { start + 1 } else { start }, + trim_prev, + trim_next, }) } _ => unreachable!(), diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 097eaa3b..15f19299 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -979,6 +979,11 @@ impl<'s> DocGen<'s> for JinjaInterpolation<'s> { F: for<'a> FnMut(&'a str, Hints) -> Result, E>, { Doc::text("{{") + .append(if self.trim_prev { + Doc::text("-") + } else { + Doc::nil() + }) .append(Doc::line_or_space()) .concat(reflow_with_indent( ctx.format_jinja(self.expr, self.start, true, state).trim(), @@ -986,6 +991,11 @@ impl<'s> DocGen<'s> for JinjaInterpolation<'s> { )) .nest(ctx.indent_width) .append(Doc::line_or_space()) + .append(if self.trim_next { + Doc::text("-") + } else { + Doc::nil() + }) .append(Doc::text("}}")) .group() } diff --git a/markup_fmt/tests/fmt/jinja/interpolation/fixture.jinja b/markup_fmt/tests/fmt/jinja/interpolation/fixture.jinja index bd6dac05..64f8923a 100644 --- a/markup_fmt/tests/fmt/jinja/interpolation/fixture.jinja +++ b/markup_fmt/tests/fmt/jinja/interpolation/fixture.jinja @@ -36,3 +36,7 @@ <{{ tag_name }}> Hello <{{ tag_name }} > Hello <{{ tag_name }}> very-very-very-very-very-very-very-very-very-long-title + +{{- example }} +{{ example -}} +{{- example -}} diff --git a/markup_fmt/tests/fmt/jinja/interpolation/fixture.snap b/markup_fmt/tests/fmt/jinja/interpolation/fixture.snap index eb257655..66adb100 100644 --- a/markup_fmt/tests/fmt/jinja/interpolation/fixture.snap +++ b/markup_fmt/tests/fmt/jinja/interpolation/fixture.snap @@ -49,3 +49,7 @@ source: markup_fmt/tests/fmt.rs <{{ tag_name }}> very-very-very-very-very-very-very-very-very-long-title + +{{- example }} +{{ example -}} +{{- example -}} From 5f73f4f41e372514341c7d3245da37c2d6062b33 Mon Sep 17 00:00:00 2001 From: Thibaut Decombe <68703331+UnknownPlatypus@users.noreply.github.com> Date: Mon, 12 Jan 2026 01:12:18 +0100 Subject: [PATCH 22/67] refactor: add `peek_pos` helper function (#188) --- markup_fmt/src/parser.rs | 68 +++++++++++----------------------------- 1 file changed, 18 insertions(+), 50 deletions(-) diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index 8e53c4c1..7d63ee4c 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -62,12 +62,16 @@ impl<'s> Parser<'s> { result } - fn emit_error(&mut self, kind: SyntaxErrorKind) -> SyntaxError { - let pos = self - .chars + #[inline] + fn peek_pos(&mut self) -> usize { + self.chars .peek() - .map(|(pos, _)| *pos) - .unwrap_or(self.source.len()); + .map(|(i, _)| *i) + .unwrap_or(self.source.len()) + } + + fn emit_error(&mut self, kind: SyntaxErrorKind) -> SyntaxError { + let pos = self.peek_pos(); self.emit_error_with_pos(kind, pos) } @@ -107,17 +111,9 @@ impl<'s> Parser<'s> { where F: FnOnce(&mut Self) -> PResult, { - let start = self - .chars - .peek() - .map(|(i, _)| *i) - .unwrap_or(self.source.len()); + let start = self.peek_pos(); let parsed = parser(self)?; - let end = self - .chars - .peek() - .map(|(i, _)| *i) - .unwrap_or(self.source.len()); + let end = self.peek_pos(); Ok((parsed, unsafe { self.source.get_unchecked(start..end) })) } @@ -464,11 +460,7 @@ impl<'s> Parser<'s> { return Err(self.emit_error(SyntaxErrorKind::ExpectChar('='))); } self.skip_ws(); - let start = self - .chars - .peek() - .map(|(i, _)| *i) - .unwrap_or(self.source.len()); + let start = self.peek_pos(); let expr = self.parse_angular_inline_script(start)?; if self.chars.next_if(|(_, c)| *c == ';').is_none() { return Err(self.emit_error(SyntaxErrorKind::ExpectChar(';'))); @@ -616,11 +608,7 @@ impl<'s> Parser<'s> { let mut children = Vec::with_capacity(1); let mut has_line_comment = false; let mut pair_stack = vec![]; - let mut pos = self - .chars - .peek() - .map(|(i, _)| *i) - .unwrap_or(self.source.len()); + let mut pos = self.peek_pos(); while let Some((i, c)) = self.chars.peek() { match c { '{' => { @@ -678,11 +666,7 @@ impl<'s> Parser<'s> { )); children.push(AstroExprChild::Template(vec![node])); } - pos = self - .chars - .peek() - .map(|(i, _)| *i) - .unwrap_or(self.source.len()); + pos = self.peek_pos(); } else { self.chars.next(); } @@ -1814,11 +1798,7 @@ impl<'s> Parser<'s> { } fn parse_raw_text_node(&mut self, tag_name: &str) -> PResult> { - let start = self - .chars - .peek() - .map(|(i, _)| *i) - .unwrap_or(self.source.len()); + let start = self.peek_pos(); let allow_nested = tag_name.eq_ignore_ascii_case("pre"); let mut nested = 0u16; @@ -1962,11 +1942,7 @@ impl<'s> Parser<'s> { self.skip_ws(); let expr = { - let start = self - .chars - .peek() - .map(|(i, _)| *i) - .unwrap_or(self.source.len()); + let start = self.peek_pos(); let mut end = start; let mut braces_stack = 0u8; loop { @@ -2221,11 +2197,7 @@ impl<'s> Parser<'s> { let mut binding = None; let expr = { - let start = self - .chars - .peek() - .map(|(i, _)| *i) - .unwrap_or(self.source.len()); + let start = self.peek_pos(); let mut end = start; let mut pair_stack = vec![]; loop { @@ -2498,11 +2470,7 @@ impl<'s> Parser<'s> { fn parse_svelte_or_astro_expr(&mut self) -> PResult<(&'s str, usize)> { self.skip_ws(); - let start = self - .chars - .peek() - .map(|(i, _)| *i) - .unwrap_or(self.source.len()); + let start = self.peek_pos(); let mut end = start; let mut braces_stack = 0u8; loop { From 3ea472d1e58697d22f0fb49bfa6dc5bff5b2e4c0 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Thu, 15 Jan 2026 20:06:13 +0800 Subject: [PATCH 23/67] ci: remove Netlify build --- .github/workflows/CI.yml | 32 +------------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1a5cc89c..028c6ee8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -15,35 +15,5 @@ jobs: name: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - uses: actions/checkout@v6 - run: cargo test --all-features - - docs: - name: documentation - if: ${{ github.event_name == 'push' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install mdbook - run: | - tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') - url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" - mkdir mdbook - curl -sSL $url | tar -xz --directory=./mdbook - echo `pwd`/mdbook >> $GITHUB_PATH - - run: mdbook build docs - - name: Deploy Production - run: | - npm i --location=global netlify-cli - netlify deploy --dir=docs/book --site=3b07be0f-5f0e-4df0-a2e6-b40f84daccac --prod - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} From 265321926666bf02a7f2b8b49cff560d9d6909e8 Mon Sep 17 00:00:00 2001 From: Thibaut Decombe <68703331+UnknownPlatypus@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:16:35 +0100 Subject: [PATCH 24/67] refactor: derive `Copy` on enums (#191) --- markup_fmt/src/config.rs | 22 +++++++++++----------- markup_fmt/src/ctx.rs | 3 +-- markup_fmt/src/lib.rs | 2 +- markup_fmt/src/printer.rs | 8 ++++---- markup_fmt/tests/fmt.rs | 4 ++-- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/markup_fmt/src/config.rs b/markup_fmt/src/config.rs index fd317a92..e700f03d 100644 --- a/markup_fmt/src/config.rs +++ b/markup_fmt/src/config.rs @@ -50,7 +50,7 @@ impl Default for LayoutOptions { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum LineBreak { @@ -282,7 +282,7 @@ impl Default for LanguageOptions { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum Quotes { @@ -291,7 +291,7 @@ pub enum Quotes { Single, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum ClosingTagLineBreakForEmpty { @@ -301,7 +301,7 @@ pub enum ClosingTagLineBreakForEmpty { Never, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum WhitespaceSensitivity { @@ -311,7 +311,7 @@ pub enum WhitespaceSensitivity { Ignore, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum DoctypeKeywordCase { @@ -321,7 +321,7 @@ pub enum DoctypeKeywordCase { Lower, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum VBindStyle { @@ -330,7 +330,7 @@ pub enum VBindStyle { Long, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum VOnStyle { @@ -339,7 +339,7 @@ pub enum VOnStyle { Long, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum VForDelimiterStyle { @@ -348,7 +348,7 @@ pub enum VForDelimiterStyle { Of, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum VSlotStyle { @@ -359,7 +359,7 @@ pub enum VSlotStyle { VSlot, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum VueComponentCase { @@ -374,7 +374,7 @@ pub enum VueComponentCase { KebabCase, } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "config_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "config_serde", serde(rename_all = "kebab-case"))] pub enum ScriptFormatter { diff --git a/markup_fmt/src/ctx.rs b/markup_fmt/src/ctx.rs index 734a3783..7184bfd4 100644 --- a/markup_fmt/src/ctx.rs +++ b/markup_fmt/src/ctx.rs @@ -86,8 +86,7 @@ where matches!( self.options .component_whitespace_sensitivity - .clone() - .unwrap_or(self.options.whitespace_sensitivity.clone()), + .unwrap_or(self.options.whitespace_sensitivity), WhitespaceSensitivity::Css | WhitespaceSensitivity::Strict ) } diff --git a/markup_fmt/src/lib.rs b/markup_fmt/src/lib.rs index 80caf3ab..ede68f1b 100644 --- a/markup_fmt/src/lib.rs +++ b/markup_fmt/src/lib.rs @@ -107,7 +107,7 @@ where } else { IndentKind::Space }, - line_break: options.layout.line_break.clone().into(), + line_break: options.layout.line_break.into(), width: options.layout.print_width, tab_size: options.layout.indent_width, }, diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 15f19299..127ad8f5 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -2529,14 +2529,14 @@ where .is_some_and(|name| name.eq_ignore_ascii_case("template")) { if slot == "default" { - ctx.options.default_v_slot_style.clone() + ctx.options.default_v_slot_style } else { - ctx.options.named_v_slot_style.clone() + ctx.options.named_v_slot_style } } else { - ctx.options.component_v_slot_style.clone() + ctx.options.component_v_slot_style }; - option.or(ctx.options.v_slot_style.clone()) + option.or(ctx.options.v_slot_style) } fn format_v_slot(style: VSlotStyle, slot: &str) -> Doc<'_> { diff --git a/markup_fmt/tests/fmt.rs b/markup_fmt/tests/fmt.rs index 3b9f2dc8..28a383ca 100644 --- a/markup_fmt/tests/fmt.rs +++ b/markup_fmt/tests/fmt.rs @@ -17,7 +17,7 @@ fn fmt_snapshot() { if let Some(options) = options { options.into_iter().for_each(|(option_name, options)| { - let output = run_format_test(path, &input, &options, language.clone()); + let output = run_format_test(path, &input, &options, language); build_settings(path).bind(|| { let name = path.file_stem().unwrap().to_str().unwrap(); assert_snapshot!(format!("{name}.{option_name}"), output); @@ -39,7 +39,7 @@ fn run_format_test( options: &FormatOptions, language: Language, ) -> String { - let output = format_text(&input, language.clone(), &options, |code, _| { + let output = format_text(&input, language, &options, |code, _| { Ok::<_, ()>(code.into()) }) .map_err(|err| format!("failed to format '{}': {:?}", path.display(), err)) From 0b0aa9534aaef6024385357ec8df17bbd1654d3f Mon Sep 17 00:00:00 2001 From: Thibaut Decombe <68703331+UnknownPlatypus@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:40:38 +0100 Subject: [PATCH 25/67] feat: always swap wrapping quotes if attribute contains one (#193) --- markup_fmt/src/printer.rs | 5 ++--- .../attributes/mixed-quotes/mixed-quotes.double.snap | 12 ++++++------ .../attributes/mixed-quotes/mixed-quotes.single.snap | 12 ++++++------ .../fmt/jinja/attributes/tag-or-block-as-attr.snap | 2 +- .../jinja/attributes/tag-or-block-in-attr-value.snap | 4 ++-- .../fmt/jinja/attributes/unquoted-assignment.snap | 2 +- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 127ad8f5..d2baab76 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -2679,7 +2679,6 @@ fn compute_attr_value_quote<'s, E, F>( where F: for<'a> FnMut(&'a str, Hints) -> Result, E>, { - let is_jinja = matches!(ctx.language, Language::Jinja); let has_single = attr_value.contains('\''); let has_double = attr_value.contains('"'); if has_double && has_single { @@ -2690,9 +2689,9 @@ where } else { Doc::text("'") } - } else if has_double && !is_jinja { + } else if has_double { Doc::text("'") - } else if has_single && !is_jinja { + } else if has_single { Doc::text("\"") } else if let Quotes::Double = ctx.options.quotes { Doc::text("\"") diff --git a/markup_fmt/tests/fmt/jinja/attributes/mixed-quotes/mixed-quotes.double.snap b/markup_fmt/tests/fmt/jinja/attributes/mixed-quotes/mixed-quotes.double.snap index e93f9f5f..96817975 100644 --- a/markup_fmt/tests/fmt/jinja/attributes/mixed-quotes/mixed-quotes.double.snap +++ b/markup_fmt/tests/fmt/jinja/attributes/mixed-quotes/mixed-quotes.double.snap @@ -4,7 +4,7 @@ source: markup_fmt/tests/fmt.rs
-
+
@@ -15,12 +15,12 @@ source: markup_fmt/tests/fmt.rs -
- - - +
+ + + {% for row in [1,2] %} -
  • {{ row }}
  • +
  • {{ row }}
  • {% endfor %} diff --git a/markup_fmt/tests/fmt/jinja/attributes/mixed-quotes/mixed-quotes.single.snap b/markup_fmt/tests/fmt/jinja/attributes/mixed-quotes/mixed-quotes.single.snap index 03a00d85..96817975 100644 --- a/markup_fmt/tests/fmt/jinja/attributes/mixed-quotes/mixed-quotes.single.snap +++ b/markup_fmt/tests/fmt/jinja/attributes/mixed-quotes/mixed-quotes.single.snap @@ -5,7 +5,7 @@ source: markup_fmt/tests/fmt.rs
    -
    +
    @@ -25,10 +25,10 @@ source: markup_fmt/tests/fmt.rs -
    - - - +
    + + + {% for row in [1,2] %} -
  • {{ row }}
  • +
  • {{ row }}
  • {% endfor %} diff --git a/markup_fmt/tests/fmt/jinja/attributes/tag-or-block-as-attr.snap b/markup_fmt/tests/fmt/jinja/attributes/tag-or-block-as-attr.snap index 91985b57..9c72335e 100644 --- a/markup_fmt/tests/fmt/jinja/attributes/tag-or-block-as-attr.snap +++ b/markup_fmt/tests/fmt/jinja/attributes/tag-or-block-as-attr.snap @@ -76,7 +76,7 @@ source: markup_fmt/tests/fmt.rs {% else %} data-start="08:30:00" data-end="20:30:00" - data-start_day="{% now "Y-m-d" %}" + data-start_day='{% now "Y-m-d" %}' data-saturday_start="08:30:00" data-saturday_end="20:30:00" {% endif %} diff --git a/markup_fmt/tests/fmt/jinja/attributes/tag-or-block-in-attr-value.snap b/markup_fmt/tests/fmt/jinja/attributes/tag-or-block-in-attr-value.snap index 0c470ec1..5796d6b2 100644 --- a/markup_fmt/tests/fmt/jinja/attributes/tag-or-block-in-attr-value.snap +++ b/markup_fmt/tests/fmt/jinja/attributes/tag-or-block-in-attr-value.snap @@ -1,11 +1,11 @@ --- source: markup_fmt/tests/fmt.rs --- -
    + {{ whatever }}
    Subscribe to the newsletter! \ No newline at end of file +> + +
    +
    + +
    +
    + +# regression test for https://github.com/g-plane/markup_fmt/issues/199 +
    +
    + +# regression test for https://github.com/g-plane/markup_fmt/issues/198 +
    +
    diff --git a/markup_fmt/tests/fmt/jinja/attributes/single-attr.snap b/markup_fmt/tests/fmt/jinja/attributes/single-attr.snap index 18a36f44..2f49386a 100644 --- a/markup_fmt/tests/fmt/jinja/attributes/single-attr.snap +++ b/markup_fmt/tests/fmt/jinja/attributes/single-attr.snap @@ -37,3 +37,48 @@ source: markup_fmt/tests/fmt.rs {% endif %} > + +
    +
    + +
    +
    + +# regression test for https://github.com/g-plane/markup_fmt/issues/199 +
    +
    + +# regression test for https://github.com/g-plane/markup_fmt/issues/198 +
    +
    From 3eb974b40da689c139fb8b23c137516ddbf09a2b Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 16 Mar 2026 23:56:09 +0800 Subject: [PATCH 40/67] feat: ignore script syntax errors for template languages (close #210) --- dprint_plugin/tests/integration/basic.vto | 9 +++++++++ .../tests/integration/dprint_ts/basic.vto.snap | 9 +++++++++ markup_fmt/src/ctx.rs | 18 +++++++++++++++++- markup_fmt/src/printer.rs | 11 +++++++++-- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/dprint_plugin/tests/integration/basic.vto b/dprint_plugin/tests/integration/basic.vto index a917e6b3..15d9ea08 100644 --- a/dprint_plugin/tests/integration/basic.vto +++ b/dprint_plugin/tests/integration/basic.vto @@ -24,3 +24,12 @@ background-color:{{colors . white}}; } + + + diff --git a/dprint_plugin/tests/integration/dprint_ts/basic.vto.snap b/dprint_plugin/tests/integration/dprint_ts/basic.vto.snap index eb9a94f4..4d98c30b 100644 --- a/dprint_plugin/tests/integration/dprint_ts/basic.vto.snap +++ b/dprint_plugin/tests/integration/dprint_ts/basic.vto.snap @@ -27,3 +27,12 @@ body { background-color: {{ colors.white }}; } + + + diff --git a/markup_fmt/src/ctx.rs b/markup_fmt/src/ctx.rs index 0a441286..a6934861 100644 --- a/markup_fmt/src/ctx.rs +++ b/markup_fmt/src/ctx.rs @@ -275,7 +275,23 @@ where start: usize, state: &State, ) -> Cow<'a, str> { - self.format_with_external_formatter( + match self.try_format_script(code, lang, start, state) { + Ok(formatted) => formatted, + Err(e) => { + self.external_formatter_errors.push(e); + Cow::from(code) + } + } + } + + pub(crate) fn try_format_script<'a>( + &mut self, + code: &'a str, + lang: &'b str, + start: usize, + state: &State, + ) -> Result, E> { + self.try_format_with_external_formatter( self.source .get(0..start) .unwrap_or_default() diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 4be696e9..14d94b27 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -728,8 +728,15 @@ impl<'s> DocGen<'s> for Element<'s> { } else { lang }; - let formatted = - ctx.format_script(text_node.raw, lang, text_node.start, &state); + let formatted = if matches!( + ctx.language, + Language::Jinja | Language::Mustache | Language::Vento + ) { + ctx.try_format_script(text_node.raw, lang, text_node.start, &state) + .unwrap_or_else(|_| Cow::from(text_node.raw)) + } else { + ctx.format_script(text_node.raw, lang, text_node.start, &state) + }; let doc = if matches!( ctx.options.script_formatter, Some(ScriptFormatter::Dprint) From f64cc1abdf090d30a0356bd95d00e5785d177f0d Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 16 Mar 2026 23:59:45 +0800 Subject: [PATCH 41/67] fix: fix unterminated comment --- markup_fmt/src/parser.rs | 10 +++++++--- .../fmt/html/format-comments/unterminated.enabled.snap | 4 ++++ .../tests/fmt/html/format-comments/unterminated.html | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 markup_fmt/tests/fmt/html/format-comments/unterminated.enabled.snap create mode 100644 markup_fmt/tests/fmt/html/format-comments/unterminated.html diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index 16abf6bf..a27ce95f 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -948,7 +948,7 @@ impl<'s> Parser<'s> { }; let start = start + 1; - let mut end = start; + let end; loop { match self.chars.next() { Some((i, '-')) => { @@ -964,7 +964,10 @@ impl<'s> Parser<'s> { } } Some(..) => continue, - None => break, + None => { + end = self.source.len(); + break; + } } } @@ -1392,7 +1395,8 @@ impl<'s> Parser<'s> { let mut body = vec![JinjaTagOrChildren::Tag(first_tag)]; loop { - let mut children = self.parse_jinja_block_children(tag_name, tag_start, children_parser)?; + let mut children = + self.parse_jinja_block_children(tag_name, tag_start, children_parser)?; if !children.is_empty() { if let Some(JinjaTagOrChildren::Children(nodes)) = body.last_mut() { nodes.append(&mut children); diff --git a/markup_fmt/tests/fmt/html/format-comments/unterminated.enabled.snap b/markup_fmt/tests/fmt/html/format-comments/unterminated.enabled.snap new file mode 100644 index 00000000..8019fa18 --- /dev/null +++ b/markup_fmt/tests/fmt/html/format-comments/unterminated.enabled.snap @@ -0,0 +1,4 @@ +--- +source: markup_fmt/tests/fmt.rs +--- + diff --git a/markup_fmt/tests/fmt/html/format-comments/unterminated.html b/markup_fmt/tests/fmt/html/format-comments/unterminated.html new file mode 100644 index 00000000..f63eda6c --- /dev/null +++ b/markup_fmt/tests/fmt/html/format-comments/unterminated.html @@ -0,0 +1 @@ + +
    +

    +
    +
    +

    +
    + + + +
    + First name: + +
    Last name: + +
    + +
    + + + +
    + + +
    +
    +
    diff --git a/markup_fmt/tests/fmt/html/attributes/accept-charset.snap b/markup_fmt/tests/fmt/html/attributes/accept-charset.snap new file mode 100644 index 00000000..95bbc8d4 --- /dev/null +++ b/markup_fmt/tests/fmt/html/attributes/accept-charset.snap @@ -0,0 +1,32 @@ +--- +source: markup_fmt/tests/fmt.rs +--- + +
    +

    +
    +
    +

    +
    + + +
    + First name: + +
    Last name: + +
    + +
    + + +
    + + +
    +
    +
    diff --git a/markup_fmt/tests/fmt/html/attributes/aria-attrs.html b/markup_fmt/tests/fmt/html/attributes/aria-attrs.html new file mode 100644 index 00000000..16107e82 --- /dev/null +++ b/markup_fmt/tests/fmt/html/attributes/aria-attrs.html @@ -0,0 +1,28 @@ + +

    Welcome to Our Website

    +

    This website is designed to be accessible for everyone.

    +
    +

    Here you will find various resources and information.

    +
    + + + +

    Click the button to subscribe to our newsletter.

    + + + + + + + diff --git a/markup_fmt/tests/fmt/html/attributes/aria-attrs.snap b/markup_fmt/tests/fmt/html/attributes/aria-attrs.snap new file mode 100644 index 00000000..317e1115 --- /dev/null +++ b/markup_fmt/tests/fmt/html/attributes/aria-attrs.snap @@ -0,0 +1,36 @@ +--- +source: markup_fmt/tests/fmt.rs +snapshot_kind: text +--- + +

    Welcome to Our Website

    +

    This website is designed to be accessible for everyone.

    +
    +

    Here you will find various resources and information.

    +
    + + + +

    Click the button to subscribe to our newsletter.

    + + + + + + + diff --git a/markup_fmt/tests/fmt/html/attributes/autocomplete.html b/markup_fmt/tests/fmt/html/attributes/autocomplete.html new file mode 100644 index 00000000..957d1d8a --- /dev/null +++ b/markup_fmt/tests/fmt/html/attributes/autocomplete.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + +
    +
    +
    +
    + + +
    diff --git a/markup_fmt/tests/fmt/html/attributes/autocomplete.snap b/markup_fmt/tests/fmt/html/attributes/autocomplete.snap new file mode 100644 index 00000000..fbd1715d --- /dev/null +++ b/markup_fmt/tests/fmt/html/attributes/autocomplete.snap @@ -0,0 +1,62 @@ +--- +source: markup_fmt/tests/fmt.rs +snapshot_kind: text +--- + + + + + + + + + + + +
    +
    +
    +
    +
    +
    + +
    diff --git a/markup_fmt/tests/fmt/html/attributes/iframe.html b/markup_fmt/tests/fmt/html/attributes/iframe.html new file mode 100644 index 00000000..c8d04084 --- /dev/null +++ b/markup_fmt/tests/fmt/html/attributes/iframe.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + +
    diff --git a/markup_fmt/tests/fmt/html/attributes/iframe.snap b/markup_fmt/tests/fmt/html/attributes/iframe.snap new file mode 100644 index 00000000..daaeb952 --- /dev/null +++ b/markup_fmt/tests/fmt/html/attributes/iframe.snap @@ -0,0 +1,24 @@ +--- +source: markup_fmt/tests/fmt.rs +--- + + + + + + + + + + + + + + + + + + +
    diff --git a/markup_fmt/tests/fmt/html/attributes/rel.html b/markup_fmt/tests/fmt/html/attributes/rel.html new file mode 100644 index 00000000..e8e43d59 --- /dev/null +++ b/markup_fmt/tests/fmt/html/attributes/rel.html @@ -0,0 +1,31 @@ +Link +Link +Link + +
    + + +
    + + + + + + + + + +Link +
    + + +
    + + +
    Link
    diff --git a/markup_fmt/tests/fmt/html/attributes/rel.snap b/markup_fmt/tests/fmt/html/attributes/rel.snap new file mode 100644 index 00000000..e70a2497 --- /dev/null +++ b/markup_fmt/tests/fmt/html/attributes/rel.snap @@ -0,0 +1,33 @@ +--- +source: markup_fmt/tests/fmt.rs +snapshot_kind: text +--- +Link +Link +Link + +
    + + +
    + + + + + + + + +Link +
    + + +
    + +
    Link
    From d993880fcfab19f6799d7580b4e238ead0f96386 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Thu, 19 Mar 2026 09:48:22 +0800 Subject: [PATCH 43/67] chore: rustfmt --- markup_fmt/src/helpers.rs | 64 +++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/markup_fmt/src/helpers.rs b/markup_fmt/src/helpers.rs index 88282d4d..20fe2d7f 100644 --- a/markup_fmt/src/helpers.rs +++ b/markup_fmt/src/helpers.rs @@ -265,49 +265,41 @@ pub(crate) fn should_be_space_separated(attr_name: &str, tag_name: Option<&str>) { true } else if attr_name.eq_ignore_ascii_case("rel") { - tag_name - .is_some_and(|name| { - ["form", "a", "area", "link"] - .iter() - .any(|tag| tag.eq_ignore_ascii_case(name)) - }) + tag_name.is_some_and(|name| { + ["form", "a", "area", "link"] + .iter() + .any(|tag| tag.eq_ignore_ascii_case(name)) + }) } else if attr_name.eq_ignore_ascii_case("blocking") { - tag_name - .is_some_and(|name| { - ["link", "script", "style"] - .iter() - .any(|tag| tag.eq_ignore_ascii_case(name)) - }) + tag_name.is_some_and(|name| { + ["link", "script", "style"] + .iter() + .any(|tag| tag.eq_ignore_ascii_case(name)) + }) } else if attr_name.eq_ignore_ascii_case("for") { - tag_name - .is_some_and(|name| name.eq_ignore_ascii_case("output")) + tag_name.is_some_and(|name| name.eq_ignore_ascii_case("output")) } else if attr_name.eq_ignore_ascii_case("headers") { - tag_name - .is_some_and(|name| { - ["td", "th"] - .iter() - .any(|tag| tag.eq_ignore_ascii_case(name)) - }) + tag_name.is_some_and(|name| { + ["td", "th"] + .iter() + .any(|tag| tag.eq_ignore_ascii_case(name)) + }) } else if attr_name.eq_ignore_ascii_case("autocomplete") { - tag_name - .is_some_and(|name| { - ["form", "input", "select", "textarea"] - .iter() - .any(|tag| tag.eq_ignore_ascii_case(name)) - }) + tag_name.is_some_and(|name| { + ["form", "input", "select", "textarea"] + .iter() + .any(|tag| tag.eq_ignore_ascii_case(name)) + }) } else if attr_name.eq_ignore_ascii_case("sandbox") { - tag_name - .is_some_and(|name| name.eq_ignore_ascii_case("iframe")) + tag_name.is_some_and(|name| name.eq_ignore_ascii_case("iframe")) } else if attr_name.eq_ignore_ascii_case("accept-charset") { - tag_name - .is_some_and(|name| name.eq_ignore_ascii_case("form")) + tag_name.is_some_and(|name| name.eq_ignore_ascii_case("form")) } else if attr_name.eq_ignore_ascii_case("ping") { - tag_name - .is_some_and(|name| { - ["a", "area"] - .iter() - .any(|tag| tag.eq_ignore_ascii_case(name)) - }) + tag_name.is_some_and(|name| { + ["a", "area"] + .iter() + .any(|tag| tag.eq_ignore_ascii_case(name)) + }) } else { false } From c56e88d9267c70556d86df54f48bf96ec442fac1 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Thu, 19 Mar 2026 09:50:52 +0800 Subject: [PATCH 44/67] ci: check formatting --- .github/workflows/CI.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e7b1da5e..71e277cf 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -16,5 +16,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - run: cargo fmt --check - run: cargo clippy --all-targets --all-features - run: cargo test --all-features From 377a9c707e4fbd75ae3a8239bbd28d07dfcb72c9 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Thu, 19 Mar 2026 09:52:35 +0800 Subject: [PATCH 45/67] fix: fix converting pascal case to kebab case (fix #213) --- markup_fmt/src/helpers.rs | 2 +- markup_fmt/tests/fmt/vue/component-case/case.default.snap | 1 + markup_fmt/tests/fmt/vue/component-case/case.kebab.snap | 1 + markup_fmt/tests/fmt/vue/component-case/case.pascal.snap | 1 + markup_fmt/tests/fmt/vue/component-case/case.vue | 1 + 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/markup_fmt/src/helpers.rs b/markup_fmt/src/helpers.rs index 20fe2d7f..b020e2a1 100644 --- a/markup_fmt/src/helpers.rs +++ b/markup_fmt/src/helpers.rs @@ -196,7 +196,7 @@ pub(crate) fn pascal2kebab(s: &'_ str) -> Cow<'_, str> { { let mut result = String::with_capacity(s.len() + uppers); s.chars().fold('<', |prev, c| { - if c.is_ascii_uppercase() && prev.is_ascii_lowercase() { + if c.is_ascii_uppercase() && prev.is_ascii_alphanumeric() { result.push('-'); } result.push(c.to_ascii_lowercase()); diff --git a/markup_fmt/tests/fmt/vue/component-case/case.default.snap b/markup_fmt/tests/fmt/vue/component-case/case.default.snap index 7c3a1a6f..b853e0fd 100644 --- a/markup_fmt/tests/fmt/vue/component-case/case.default.snap +++ b/markup_fmt/tests/fmt/vue/component-case/case.default.snap @@ -17,4 +17,5 @@ source: markup_fmt/tests/fmt.rs + diff --git a/markup_fmt/tests/fmt/vue/component-case/case.kebab.snap b/markup_fmt/tests/fmt/vue/component-case/case.kebab.snap index e930c3b5..ea913931 100644 --- a/markup_fmt/tests/fmt/vue/component-case/case.kebab.snap +++ b/markup_fmt/tests/fmt/vue/component-case/case.kebab.snap @@ -17,4 +17,5 @@ source: markup_fmt/tests/fmt.rs + diff --git a/markup_fmt/tests/fmt/vue/component-case/case.pascal.snap b/markup_fmt/tests/fmt/vue/component-case/case.pascal.snap index 00c10fec..f2696684 100644 --- a/markup_fmt/tests/fmt/vue/component-case/case.pascal.snap +++ b/markup_fmt/tests/fmt/vue/component-case/case.pascal.snap @@ -17,4 +17,5 @@ source: markup_fmt/tests/fmt.rs + diff --git a/markup_fmt/tests/fmt/vue/component-case/case.vue b/markup_fmt/tests/fmt/vue/component-case/case.vue index e0735fd9..043f798c 100644 --- a/markup_fmt/tests/fmt/vue/component-case/case.vue +++ b/markup_fmt/tests/fmt/vue/component-case/case.vue @@ -14,4 +14,5 @@ + From 60d3c4606993f2facb5c49085e42895d30aba9e3 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 24 Mar 2026 16:11:35 +0800 Subject: [PATCH 46/67] feat: support comments in Svelte attributes --- markup_fmt/src/ast.rs | 7 ++ markup_fmt/src/parser.rs | 65 ++++++++++++++++--- markup_fmt/src/printer.rs | 17 +++++ .../tests/fmt/svelte/comments/attrs.snap | 26 ++++++++ .../tests/fmt/svelte/comments/attrs.svelte | 22 +++++++ 5 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 markup_fmt/tests/fmt/svelte/comments/attrs.snap create mode 100644 markup_fmt/tests/fmt/svelte/comments/attrs.svelte diff --git a/markup_fmt/src/ast.rs b/markup_fmt/src/ast.rs index b5764216..8f4c7e0a 100644 --- a/markup_fmt/src/ast.rs +++ b/markup_fmt/src/ast.rs @@ -109,6 +109,7 @@ pub enum Attribute<'s> { JinjaBlock(JinjaBlock<'s, Attribute<'s>>), JinjaComment(JinjaComment<'s>), JinjaTag(JinjaTag<'s>), + JsComment(JsComment<'s>), Native(NativeAttribute<'s>), Svelte(SvelteAttribute<'s>), SvelteAttachment(SvelteAttachment<'s>), @@ -205,6 +206,12 @@ pub enum JinjaTagOrChildren<'s, T> { Children(Vec), } +#[derive(Debug)] +pub struct JsComment<'s> { + pub block: bool, + pub raw: &'s str, +} + #[derive(Debug)] /// Mustache block: `{{#variable}}{{/variable}}`. /// diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index a27ce95f..c65055c9 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -1019,17 +1019,23 @@ impl<'s> Parser<'s> { match self.chars.peek() { Some((_, '/')) => { self.chars.next(); - if self.chars.next_if(|(_, c)| *c == '>').is_some() { - return Ok(Element { - tag_name, - attrs, - first_attr_same_line, - children: vec![], - self_closing: true, - void_element, - }); + match self.chars.peek() { + Some((_, '>')) => { + self.chars.next(); + return Ok(Element { + tag_name, + attrs, + first_attr_same_line, + children: vec![], + self_closing: true, + void_element, + }); + } + Some((_, '/' | '*')) if self.language == Language::Svelte => { + attrs.push(Attribute::JsComment(self.parse_js_comment()?)); + } + _ => return Err(self.emit_error(SyntaxErrorKind::ExpectSelfCloseTag)), } - return Err(self.emit_error(SyntaxErrorKind::ExpectSelfCloseTag)); } Some((_, '>')) => { self.chars.next(); @@ -1442,6 +1448,45 @@ impl<'s> Parser<'s> { } } + // former `/` has been consumed + fn parse_js_comment(&mut self) -> PResult> { + let Some((start, first_char)) = self.chars.next_if(|(_, c)| matches!(c, '/' | '*')) else { + return Err(self.emit_error(SyntaxErrorKind::ExpectChar('/'))); + }; + let start = start + 1; + if first_char == '/' { + let mut end = start; + loop { + match self.chars.next() { + Some((_, '\n')) | None => break, + Some((i, _)) => end = i, + } + } + Ok(JsComment { + block: false, + raw: unsafe { self.source.get_unchecked(start..=end) }, + }) + } else { + let mut end = start; + loop { + match self.chars.next() { + Some((i, '*')) => { + end = i; + if self.chars.next_if(|(_, c)| *c == '/').is_some() { + break; + } + } + Some((i, _)) => end = i, + None => break, + } + } + Ok(JsComment { + block: true, + raw: unsafe { self.source.get_unchecked(start..end) }, + }) + } + } + fn parse_mustache_block_or_interpolation(&mut self) -> PResult> { let mut controls = vec![]; let (raw, _) = self.parse_mustache_interpolation()?; diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 43e15914..3b807e77 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -370,6 +370,7 @@ impl<'s> DocGen<'s> for Attribute<'s> { Attribute::JinjaComment(jinja_comment) => jinja_comment.doc(ctx, state), Attribute::JinjaTag(jinja_tag) => jinja_tag.doc(ctx, state), Attribute::VentoTagOrBlock(vento_tag_or_block) => vento_tag_or_block.doc(ctx, state), + Attribute::JsComment(js_comment) => js_comment.doc(ctx, state), } } } @@ -1051,6 +1052,21 @@ impl<'s> DocGen<'s> for JinjaTag<'s> { } } +impl<'s> DocGen<'s> for JsComment<'s> { + fn doc(&self, _: &mut Ctx<'s, E, F>, _: &State<'s>) -> Doc<'s> + where + F: for<'a> FnMut(&'a str, Hints) -> Result, E>, + { + if self.block { + Doc::text("/*") + .concat(reflow_raw(self.raw)) + .append(Doc::text("*/")) + } else { + Doc::text(format!("//{}", self.raw.trim_ascii_end())) + } + } +} + impl<'s> DocGen<'s> for MustacheBlock<'s> { fn doc(&self, ctx: &mut Ctx<'s, E, F>, state: &State<'s>) -> Doc<'s> where @@ -2273,6 +2289,7 @@ fn is_multi_line_attr(attr: &Attribute) -> bool { | Attribute::JinjaTag(JinjaTag { content: value, .. }) => value.contains('\n'), // Templating blocks usually span across multiple lines so let's just assume true. Attribute::JinjaBlock(..) | Attribute::VentoTagOrBlock(..) => true, + Attribute::JsComment(comment) => comment.raw.contains('\n'), } } diff --git a/markup_fmt/tests/fmt/svelte/comments/attrs.snap b/markup_fmt/tests/fmt/svelte/comments/attrs.snap new file mode 100644 index 00000000..89608eea --- /dev/null +++ b/markup_fmt/tests/fmt/svelte/comments/attrs.snap @@ -0,0 +1,26 @@ +--- +source: markup_fmt/tests/fmt.rs +--- +
    +
    + + diff --git a/markup_fmt/tests/fmt/svelte/comments/attrs.svelte b/markup_fmt/tests/fmt/svelte/comments/attrs.svelte new file mode 100644 index 00000000..ae83ed5e --- /dev/null +++ b/markup_fmt/tests/fmt/svelte/comments/attrs.svelte @@ -0,0 +1,22 @@ +
    + + From 40a8bf949264e26928ce9b0da7e27bea868fb25d Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 24 Mar 2026 16:15:45 +0800 Subject: [PATCH 47/67] v0.27.0 --- Cargo.lock | 4 ++-- README.md | 2 +- dprint_plugin/Cargo.toml | 2 +- dprint_plugin/deployment/npm/package.json | 2 +- dprint_plugin/deployment/schema.json | 2 +- markup_fmt/Cargo.toml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 504ed7e0..299b86b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,7 +306,7 @@ dependencies = [ [[package]] name = "dprint_plugin_markup" -version = "0.26.0" +version = "0.27.0" dependencies = [ "anyhow", "dprint-core", @@ -614,7 +614,7 @@ dependencies = [ [[package]] name = "markup_fmt" -version = "0.26.0" +version = "0.27.0" dependencies = [ "aho-corasick", "anyhow", diff --git a/README.md b/README.md index 1eb7ab20..e0d71fbe 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ After adding the dprint plugins, update your `dprint.json` and add configuration // ... "plugins": [ // ... other plugins URL - "https://plugins.dprint.dev/g-plane/markup_fmt-v0.26.0.wasm" + "https://plugins.dprint.dev/g-plane/markup_fmt-v0.27.0.wasm" ], "markup": { // <-- the key name here is "markup", not "markup_fmt" // config comes here diff --git a/dprint_plugin/Cargo.toml b/dprint_plugin/Cargo.toml index 82ba9674..4a96cefd 100644 --- a/dprint_plugin/Cargo.toml +++ b/dprint_plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dprint_plugin_markup" -version = "0.26.0" +version = "0.27.0" edition = "2024" authors = ["Pig Fang "] description = "markup_fmt as dprint plugin." diff --git a/dprint_plugin/deployment/npm/package.json b/dprint_plugin/deployment/npm/package.json index c6a8cc78..d68e32f0 100644 --- a/dprint_plugin/deployment/npm/package.json +++ b/dprint_plugin/deployment/npm/package.json @@ -1,7 +1,7 @@ { "name": "dprint-plugin-markup", "description": "markup_fmt as dprint plugin.", - "version": "0.26.0", + "version": "0.27.0", "author": "Pig Fang ", "repository": "g-plane/markup_fmt", "license": "MIT", diff --git a/dprint_plugin/deployment/schema.json b/dprint_plugin/deployment/schema.json index 92d57b0e..75f1ef3d 100644 --- a/dprint_plugin/deployment/schema.json +++ b/dprint_plugin/deployment/schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://plugins.dprint.dev/g-plane/markup_fmt/v0.26.0/schema.json", + "$id": "https://plugins.dprint.dev/g-plane/markup_fmt/v0.27.0/schema.json", "title": "Config", "description": "Configuration for dprint-plugin-markup.", "type": "object", diff --git a/markup_fmt/Cargo.toml b/markup_fmt/Cargo.toml index 7468df1c..cbfde03c 100644 --- a/markup_fmt/Cargo.toml +++ b/markup_fmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "markup_fmt" -version = "0.26.0" +version = "0.27.0" edition = "2024" authors = ["Pig Fang "] description = "Configurable HTML, Vue, Svelte, Astro, Angular, Jinja, Twig, Nunjucks, Vento, Mustache, Handlebars and XML formatter." From 46b49f11ba91bf725ec62cb1d2a2fd60ff341e3d Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 21 Apr 2026 10:31:51 +0800 Subject: [PATCH 48/67] docs: update about `scriptFormatter` option --- docs/src/config/script-formatter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/config/script-formatter.md b/docs/src/config/script-formatter.md index 2bf82475..1d1235c7 100644 --- a/docs/src/config/script-formatter.md +++ b/docs/src/config/script-formatter.md @@ -5,6 +5,6 @@ Tell markup_fmt what script formatter you are using. Possible options: - `"dprint"`: Use this if you're using dprint-plugin-typescript. -- `"biome"`: Use this if you're using Biome or dprint-plugin-biome. +- `"biome"`: Use this if you're using Biome (dprint-plugin-biome) or Oxc (dprint-plugin-oxc). In dprint, default option is `"dprint"`; otherwise, default option is `null`. From c05139af7b16effcb757c26aa0a92a00afc65c52 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 28 Apr 2026 09:51:20 +0800 Subject: [PATCH 49/67] fix: allow JS keywords in Vue binding (fix #217) --- markup_fmt/src/printer.rs | 45 +++++++++++-------- .../tests/fmt/vue/v-bind/fixture.default.snap | 1 + .../tests/fmt/vue/v-bind/fixture.long.snap | 1 + .../tests/fmt/vue/v-bind/fixture.short.snap | 1 + markup_fmt/tests/fmt/vue/v-bind/fixture.vue | 1 + 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 3b807e77..9031c91b 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -2068,7 +2068,7 @@ impl<'s> DocGen<'s> for VueDirective<'s> { } else { "in" }; - format_v_for(left, delimiter, right, value_start, ctx) + Cow::from(format_v_for(left, delimiter, right, value_start, ctx)) } else if let Some((left, right)) = value.split_once(" of ") { let delimiter = if let Some(VForDelimiterStyle::In) = ctx.options.v_for_delimiter_style @@ -2077,28 +2077,35 @@ impl<'s> DocGen<'s> for VueDirective<'s> { } else { "of" }; - format_v_for(left, delimiter, right, value_start, ctx) + Cow::from(format_v_for(left, delimiter, right, value_start, ctx)) } else { - ctx.with_escaping_quotes(value, |code, ctx| { + Cow::from(ctx.with_escaping_quotes(value, |code, ctx| { ctx.format_expr(&code, true, value_start) - }) + })) + } + } + "#" | "slot" => Cow::from(ctx.format_binding(value, value_start)), + _ => { + if value.bytes().all(|b| b.is_ascii_alphanumeric()) { + // not only a shorthand but also allowing JavaScript keywords + Cow::from(value) + } else { + Cow::from(ctx.with_escaping_quotes(value, |code, ctx| { + ctx.try_format_expr(&code, true, value_start) + .unwrap_or_else(|_| { + let formatted = ctx + .format_script(&code, "ts", value_start, state) + .trim() + .to_owned(); + if formatted.contains('\n') { + formatted + } else { + formatted.trim_end_matches(';').to_owned() + } + }) + })) } } - "#" | "slot" => ctx.format_binding(value, value_start), - _ => ctx.with_escaping_quotes(value, |code, ctx| { - ctx.try_format_expr(&code, true, value_start) - .unwrap_or_else(|_| { - let formatted = ctx - .format_script(&code, "ts", value_start, state) - .trim() - .to_owned(); - if formatted.contains('\n') { - formatted - } else { - formatted.trim_end_matches(';').to_owned() - } - }) - }), }; if !(matches!(ctx.options.v_bind_same_name_short_hand, Some(true)) && is_v_bind diff --git a/markup_fmt/tests/fmt/vue/v-bind/fixture.default.snap b/markup_fmt/tests/fmt/vue/v-bind/fixture.default.snap index a8c4019f..6579dd58 100644 --- a/markup_fmt/tests/fmt/vue/v-bind/fixture.default.snap +++ b/markup_fmt/tests/fmt/vue/v-bind/fixture.default.snap @@ -8,4 +8,5 @@ source: markup_fmt/tests/fmt.rs + diff --git a/markup_fmt/tests/fmt/vue/v-bind/fixture.long.snap b/markup_fmt/tests/fmt/vue/v-bind/fixture.long.snap index e9ef1ddd..371dd09e 100644 --- a/markup_fmt/tests/fmt/vue/v-bind/fixture.long.snap +++ b/markup_fmt/tests/fmt/vue/v-bind/fixture.long.snap @@ -8,4 +8,5 @@ source: markup_fmt/tests/fmt.rs + diff --git a/markup_fmt/tests/fmt/vue/v-bind/fixture.short.snap b/markup_fmt/tests/fmt/vue/v-bind/fixture.short.snap index debb478e..78cebe99 100644 --- a/markup_fmt/tests/fmt/vue/v-bind/fixture.short.snap +++ b/markup_fmt/tests/fmt/vue/v-bind/fixture.short.snap @@ -8,4 +8,5 @@ source: markup_fmt/tests/fmt.rs + diff --git a/markup_fmt/tests/fmt/vue/v-bind/fixture.vue b/markup_fmt/tests/fmt/vue/v-bind/fixture.vue index 5f29afc5..c603b77d 100644 --- a/markup_fmt/tests/fmt/vue/v-bind/fixture.vue +++ b/markup_fmt/tests/fmt/vue/v-bind/fixture.vue @@ -5,4 +5,5 @@ + From e76c7beb698843257721a28ab97bcade86006a39 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 8 May 2026 10:46:07 +0800 Subject: [PATCH 50/67] fix: fix broken Astro front matter (fix #220) --- markup_fmt/src/parser.rs | 2 +- markup_fmt/tests/fmt/astro/front-matter/issue-220.astro | 3 +++ markup_fmt/tests/fmt/astro/front-matter/issue-220.snap | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 markup_fmt/tests/fmt/astro/front-matter/issue-220.astro create mode 100644 markup_fmt/tests/fmt/astro/front-matter/issue-220.snap diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index c65055c9..70c120f1 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -1192,7 +1192,7 @@ impl<'s> Parser<'s> { pair_stack.pop(); } Some((_, '/')) - if !matches!(pair_stack.last(), Some('\'' | '"' | '`' | '/' | '*')) => + if !matches!(pair_stack.last(), Some('\'' | '"' | '`' | '/' | '*' | '$')) => { if let Some((_, c)) = self.chars.next_if(|(_, c)| *c == '/' || *c == '*') { pair_stack.push(c); diff --git a/markup_fmt/tests/fmt/astro/front-matter/issue-220.astro b/markup_fmt/tests/fmt/astro/front-matter/issue-220.astro new file mode 100644 index 00000000..815adeea --- /dev/null +++ b/markup_fmt/tests/fmt/astro/front-matter/issue-220.astro @@ -0,0 +1,3 @@ +--- +`${/\//}` +--- diff --git a/markup_fmt/tests/fmt/astro/front-matter/issue-220.snap b/markup_fmt/tests/fmt/astro/front-matter/issue-220.snap new file mode 100644 index 00000000..eb6edad9 --- /dev/null +++ b/markup_fmt/tests/fmt/astro/front-matter/issue-220.snap @@ -0,0 +1,6 @@ +--- +source: markup_fmt/tests/fmt.rs +--- +--- +`${/\//}` +--- From 0a858d52b9dac9af779ba403291dc5d3da201577 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 8 May 2026 11:33:06 +0800 Subject: [PATCH 51/67] ci: release with GitHub CLI --- .github/workflows/release.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf034b6f..265c5850 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,24 +14,23 @@ jobs: name: Release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: rustup target add wasm32-unknown-unknown - run: | cargo build --release -p dprint_plugin_markup --target wasm32-unknown-unknown cp target/wasm32-unknown-unknown/release/dprint_plugin_markup.wasm dprint_plugin/deployment/plugin.wasm - name: Publish dprint plugin - uses: softprops/action-gh-release@v2 - with: - files: | - dprint_plugin/deployment/plugin.wasm - dprint_plugin/deployment/schema.json - - uses: rust-lang/crates-io-auth-action@v1 + run: | + gh release create ${{ github.ref_name }} dprint_plugin/deployment/plugin.wasm dprint_plugin/deployment/schema.json + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: rust-lang/crates-io-auth-action@v1.0.4 id: cratesio-auth - name: Publish crates run: cargo publish -p markup_fmt env: CARGO_REGISTRY_TOKEN: ${{ steps.cratesio-auth.outputs.token }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: registry-url: "https://registry.npmjs.org" - name: Publish npm package From 99fd96bddd32fa0e42bd71bf5fd781db36f0e1ce Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 8 May 2026 11:35:05 +0800 Subject: [PATCH 52/67] v0.27.1 --- Cargo.lock | 4 ++-- README.md | 2 +- dprint_plugin/Cargo.toml | 2 +- dprint_plugin/deployment/npm/package.json | 2 +- dprint_plugin/deployment/schema.json | 2 +- markup_fmt/Cargo.toml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 299b86b2..3f577981 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,7 +306,7 @@ dependencies = [ [[package]] name = "dprint_plugin_markup" -version = "0.27.0" +version = "0.27.1" dependencies = [ "anyhow", "dprint-core", @@ -614,7 +614,7 @@ dependencies = [ [[package]] name = "markup_fmt" -version = "0.27.0" +version = "0.27.1" dependencies = [ "aho-corasick", "anyhow", diff --git a/README.md b/README.md index e0d71fbe..a7163e06 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ After adding the dprint plugins, update your `dprint.json` and add configuration // ... "plugins": [ // ... other plugins URL - "https://plugins.dprint.dev/g-plane/markup_fmt-v0.27.0.wasm" + "https://plugins.dprint.dev/g-plane/markup_fmt-v0.27.1.wasm" ], "markup": { // <-- the key name here is "markup", not "markup_fmt" // config comes here diff --git a/dprint_plugin/Cargo.toml b/dprint_plugin/Cargo.toml index 4a96cefd..0f3fe35a 100644 --- a/dprint_plugin/Cargo.toml +++ b/dprint_plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dprint_plugin_markup" -version = "0.27.0" +version = "0.27.1" edition = "2024" authors = ["Pig Fang "] description = "markup_fmt as dprint plugin." diff --git a/dprint_plugin/deployment/npm/package.json b/dprint_plugin/deployment/npm/package.json index d68e32f0..2e8f9613 100644 --- a/dprint_plugin/deployment/npm/package.json +++ b/dprint_plugin/deployment/npm/package.json @@ -1,7 +1,7 @@ { "name": "dprint-plugin-markup", "description": "markup_fmt as dprint plugin.", - "version": "0.27.0", + "version": "0.27.1", "author": "Pig Fang ", "repository": "g-plane/markup_fmt", "license": "MIT", diff --git a/dprint_plugin/deployment/schema.json b/dprint_plugin/deployment/schema.json index 75f1ef3d..3a321041 100644 --- a/dprint_plugin/deployment/schema.json +++ b/dprint_plugin/deployment/schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://plugins.dprint.dev/g-plane/markup_fmt/v0.27.0/schema.json", + "$id": "https://plugins.dprint.dev/g-plane/markup_fmt/v0.27.1/schema.json", "title": "Config", "description": "Configuration for dprint-plugin-markup.", "type": "object", diff --git a/markup_fmt/Cargo.toml b/markup_fmt/Cargo.toml index cbfde03c..92f28b9f 100644 --- a/markup_fmt/Cargo.toml +++ b/markup_fmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "markup_fmt" -version = "0.27.0" +version = "0.27.1" edition = "2024" authors = ["Pig Fang "] description = "Configurable HTML, Vue, Svelte, Astro, Angular, Jinja, Twig, Nunjucks, Vento, Mustache, Handlebars and XML formatter." From 65bfac66ec40a73165b75eccbb24d75769a3fcf1 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Sun, 10 May 2026 10:01:38 +0800 Subject: [PATCH 53/67] fix: fix parsing Jinja tag name with args (fix #222) --- markup_fmt/src/parser.rs | 2 +- .../tests/fmt/jinja/control-structure/fixture.jinja | 8 +++++++- markup_fmt/tests/fmt/jinja/control-structure/fixture.snap | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index 70c120f1..0879cc4c 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -2840,7 +2840,7 @@ fn is_attr_name_char(c: char) -> bool { fn parse_jinja_tag_name<'s>(tag: &JinjaTag<'s>) -> &'s str { let trimmed = tag.content.trim_start_matches(['+', '-']).trim_start(); trimmed - .split_once(|c: char| c.is_ascii_whitespace()) + .split_once(|c: char| !c.is_ascii_alphanumeric() && c != '_') .map(|(name, _)| name) .unwrap_or(trimmed) } diff --git a/markup_fmt/tests/fmt/jinja/control-structure/fixture.jinja b/markup_fmt/tests/fmt/jinja/control-structure/fixture.jinja index a200e65d..0c35d34b 100644 --- a/markup_fmt/tests/fmt/jinja/control-structure/fixture.jinja +++ b/markup_fmt/tests/fmt/jinja/control-structure/fixture.jinja @@ -75,4 +75,10 @@ There are {{ count }} {{ name }} objects.
  • {{ item }}
  • {% endfor %} -{% endraw %} \ No newline at end of file +{% endraw %} + +{% call(user) dump_users(list_of_user) %} +
    +
    Realname
    +
    +{% endcall %} diff --git a/markup_fmt/tests/fmt/jinja/control-structure/fixture.snap b/markup_fmt/tests/fmt/jinja/control-structure/fixture.snap index 5c97eeea..52297abd 100644 --- a/markup_fmt/tests/fmt/jinja/control-structure/fixture.snap +++ b/markup_fmt/tests/fmt/jinja/control-structure/fixture.snap @@ -84,3 +84,9 @@ source: markup_fmt/tests/fmt.rs {% endfor %} {% endraw %} + +{% call(user) dump_users(list_of_user) %} +
    +
    Realname
    +
    +{% endcall %} From 360ae53fcd7bde0cc4f45c445a706f0d0f583413 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 11 May 2026 11:56:34 +0800 Subject: [PATCH 54/67] feat: support Vento `slot` and `default` tags (close #215) --- markup_fmt/src/parser.rs | 2 +- markup_fmt/tests/fmt/vento/layout/fixture.snap | 8 ++++++++ markup_fmt/tests/fmt/vento/layout/fixture.vto | 8 ++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index 0879cc4c..bff0036b 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -2636,7 +2636,7 @@ impl<'s> Parser<'s> { let is_function = tag_name == "function" || matches!(tag_name, "async" | "export") && tag_rest.starts_with("function"); - if matches!(tag_name, "for" | "if" | "layout") + if matches!(tag_name, "for" | "if" | "layout" | "slot" | "default") || matches!(tag_name, "set" | "export") && !first_tag.contains('=') || is_function { diff --git a/markup_fmt/tests/fmt/vento/layout/fixture.snap b/markup_fmt/tests/fmt/vento/layout/fixture.snap index 04519c6d..c929e22a 100644 --- a/markup_fmt/tests/fmt/vento/layout/fixture.snap +++ b/markup_fmt/tests/fmt/vento/layout/fixture.snap @@ -8,3 +8,11 @@ source: markup_fmt/tests/fmt.rs {{ layout 'container.vto' {size:'big'} }}

    Hello, world!

    {{ /layout }} + +{{ slot foo }} + Slot content +{{ /slot }} + +{{ default bar }} + Default content for bar +{{ /default }} diff --git a/markup_fmt/tests/fmt/vento/layout/fixture.vto b/markup_fmt/tests/fmt/vento/layout/fixture.vto index b3519be9..914b3755 100644 --- a/markup_fmt/tests/fmt/vento/layout/fixture.vto +++ b/markup_fmt/tests/fmt/vento/layout/fixture.vto @@ -5,3 +5,11 @@ {{layout 'container.vto'{size:'big'}}}

    Hello, world!

    {{/layout}} + +{{ slot foo }} +Slot content +{{ /slot }} + +{{ default bar }} +Default content for bar +{{ /default }} From 4acca03cc81a04e247e8dac1a32627130a019ecc Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 11 May 2026 12:02:55 +0800 Subject: [PATCH 55/67] chore: apply Clippy suggestions --- markup_fmt/src/parser.rs | 68 ++++++++++++++++++--------------------- markup_fmt/src/printer.rs | 22 ++++++------- 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index bff0036b..bbbf22df 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -870,18 +870,14 @@ impl<'s> Parser<'s> { let mut chars = self.chars.clone(); chars.next(); match chars.peek() { - Some((_, '%')) => { + Some((_, '%')) if self .parse_jinja_tag_or_block(None, &mut Parser::parse_node) - .is_ok() - { - end = - self.chars.peek().map(|(i, _)| i - 1).ok_or_else(|| { - self.emit_error(SyntaxErrorKind::ExpectAttrValue) - })?; - } else { - self.chars.next(); - } + .is_ok() => + { + end = self.chars.peek().map(|(i, _)| i - 1).ok_or_else(|| { + self.emit_error(SyntaxErrorKind::ExpectAttrValue) + })?; } Some((_, '{')) => { chars.next(); @@ -1923,28 +1919,26 @@ impl<'s> Parser<'s> { self.skip_ws(); let mut chars = self.chars.clone(); match chars.next() { - Some((_, 't')) => { + Some((_, 't')) if chars .next_if(|(_, c)| *c == 'h') .and_then(|_| chars.next_if(|(_, c)| *c == 'e')) .and_then(|_| chars.next_if(|(_, c)| *c == 'n')) - .is_some() - { - end = i; - break; - } + .is_some() => + { + end = i; + break; } - Some((_, 'c')) => { + Some((_, 'c')) if chars .next_if(|(_, c)| *c == 'a') .and_then(|_| chars.next_if(|(_, c)| *c == 't')) .and_then(|_| chars.next_if(|(_, c)| *c == 'c')) .and_then(|_| chars.next_if(|(_, c)| *c == 'h')) - .is_some() - { - end = i; - break; - } + .is_some() => + { + end = i; + break; } _ => {} } @@ -2954,12 +2948,12 @@ pub fn parse_as_interpolated( pos = i; brace_stack += 1; } - Language::Jinja | Language::Vento | Language::Mustache => { - if chars.next_if(|(_, c)| *c == '{').is_some() { - statics.push(unsafe { text.get_unchecked(pos..i) }); - pos = i; - brace_stack += 1; - } + Language::Jinja | Language::Vento | Language::Mustache + if chars.next_if(|(_, c)| *c == '{').is_some() => + { + statics.push(unsafe { text.get_unchecked(pos..i) }); + pos = i; + brace_stack += 1; } _ => {} } @@ -2978,15 +2972,15 @@ pub fn parse_as_interpolated( pos = i + 1; brace_stack = 0; } - Language::Jinja | Language::Vento | Language::Mustache => { - if chars.next_if(|(_, c)| *c == '}').is_some() { - dynamics.push(( - unsafe { text.get_unchecked(pos + 2..i) }, - base_start + pos + 2, - )); - pos = i + 2; - brace_stack = 0; - } + Language::Jinja | Language::Vento | Language::Mustache + if chars.next_if(|(_, c)| *c == '}').is_some() => + { + dynamics.push(( + unsafe { text.get_unchecked(pos + 2..i) }, + base_start + pos + 2, + )); + pos = i + 2; + brace_stack = 0; } _ => {} } diff --git a/markup_fmt/src/printer.rs b/markup_fmt/src/printer.rs index 9031c91b..37f22c1a 100644 --- a/markup_fmt/src/printer.rs +++ b/markup_fmt/src/printer.rs @@ -1884,11 +1884,9 @@ impl<'s> DocGen<'s> for VentoTag<'s> { quotes_stack.push(char); } } - '{' => { - if quotes_stack.is_empty() { - brace_index = Some(index); - break; - } + '{' if quotes_stack.is_empty() => { + brace_index = Some(index); + break; } _ => {} } @@ -2207,10 +2205,10 @@ fn reflow_with_indent<'i, 'o: 'i>( pair_stack.push(c); } } - '$' if matches!(pair_stack.last(), Some('`')) => { - if chars.next_if(|next| *next == '{').is_some() { - pair_stack.push('$'); - } + '$' if matches!(pair_stack.last(), Some('`')) + && chars.next_if(|next| *next == '{').is_some() => + { + pair_stack.push('$'); } '{' if !matches!(pair_stack.last(), Some('`' | '\'' | '"' | '/')) => { pair_stack.push('{'); @@ -2225,10 +2223,8 @@ fn reflow_with_indent<'i, 'o: 'i>( break; } } - '*' => { - if chars.next_if(|next| *next == '/').is_some() { - pair_stack.pop(); - } + '*' if chars.next_if(|next| *next == '/').is_some() => { + pair_stack.pop(); } '\\' if matches!(pair_stack.last(), Some('\'' | '"' | '`')) => { chars.next(); From d6e4aabc17b168a3bf44183b47bd6e827abd9c7f Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 11 May 2026 17:30:20 +0800 Subject: [PATCH 56/67] refactor: move position conversion to helpers --- markup_fmt/src/helpers.rs | 17 ++++++++++++++++- markup_fmt/src/parser.rs | 27 +++++++-------------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/markup_fmt/src/helpers.rs b/markup_fmt/src/helpers.rs index b020e2a1..3c22d3a3 100644 --- a/markup_fmt/src/helpers.rs +++ b/markup_fmt/src/helpers.rs @@ -1,6 +1,6 @@ use crate::Language; use aho_corasick::AhoCorasick; -use std::{borrow::Cow, sync::LazyLock}; +use std::{borrow::Cow, cmp::Ordering, ops::ControlFlow, sync::LazyLock}; pub(crate) fn is_component(name: &str) -> bool { name.contains('-') || name.contains(|c: char| c.is_ascii_uppercase()) @@ -304,3 +304,18 @@ pub(crate) fn should_be_space_separated(attr_name: &str, tag_name: Option<&str>) false } } + +pub(crate) fn pos_to_line_col(source: &str, pos: usize) -> (usize, usize) { + let search = memchr::memchr_iter(b'\n', source.as_bytes()).try_fold( + (1, 0), + |(line, prev_offset), offset| match pos.cmp(&offset) { + Ordering::Less => ControlFlow::Break((line, prev_offset)), + Ordering::Equal => ControlFlow::Break((line, prev_offset)), + Ordering::Greater => ControlFlow::Continue((line + 1, offset)), + }, + ); + match search { + ControlFlow::Break((line, offset)) => (line, pos - offset + 1), + ControlFlow::Continue((line, _)) => (line, 0), + } +} diff --git a/markup_fmt/src/parser.rs b/markup_fmt/src/parser.rs index bbbf22df..65876500 100644 --- a/markup_fmt/src/parser.rs +++ b/markup_fmt/src/parser.rs @@ -12,7 +12,7 @@ use crate::{ error::{SyntaxError, SyntaxErrorKind}, helpers, }; -use std::{cmp::Ordering, iter::Peekable, ops::ControlFlow, str::CharIndices}; +use std::{iter::Peekable, str::CharIndices}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Supported languages. @@ -76,7 +76,7 @@ impl<'s> Parser<'s> { } fn emit_error_with_pos(&self, kind: SyntaxErrorKind, pos: usize) -> SyntaxError { - let (line, column) = self.pos_to_line_col(pos); + let (line, column) = helpers::pos_to_line_col(self.source, pos); SyntaxError { kind, pos, @@ -84,20 +84,6 @@ impl<'s> Parser<'s> { column, } } - fn pos_to_line_col(&self, pos: usize) -> (usize, usize) { - let search = memchr::memchr_iter(b'\n', self.source.as_bytes()).try_fold( - (1, 0), - |(line, prev_offset), offset| match pos.cmp(&offset) { - Ordering::Less => ControlFlow::Break((line, prev_offset)), - Ordering::Equal => ControlFlow::Break((line, prev_offset)), - Ordering::Greater => ControlFlow::Continue((line + 1, offset)), - }, - ); - match search { - ControlFlow::Break((line, offset)) => (line, pos - offset + 1), - ControlFlow::Continue((line, _)) => (line, 0), - } - } fn skip_ws(&mut self) { while self @@ -1088,7 +1074,8 @@ impl<'s> Parser<'s> { self.chars = chars; let close_tag_name = self.parse_tag_name()?; if !close_tag_name.eq_ignore_ascii_case(tag_name) { - let (line, column) = self.pos_to_line_col(element_start); + let (line, column) = + helpers::pos_to_line_col(self.source, element_start); return Err(self.emit_error_with_pos( SyntaxErrorKind::ExpectCloseTag { tag_name: tag_name.into(), @@ -1102,7 +1089,7 @@ impl<'s> Parser<'s> { if self.chars.next_if(|(_, c)| *c == '>').is_some() { break; } - let (line, column) = self.pos_to_line_col(element_start); + let (line, column) = helpers::pos_to_line_col(self.source, element_start); return Err(self.emit_error(SyntaxErrorKind::ExpectCloseTag { tag_name: tag_name.into(), line, @@ -1126,7 +1113,7 @@ impl<'s> Parser<'s> { } } None => { - let (line, column) = self.pos_to_line_col(element_start); + let (line, column) = helpers::pos_to_line_col(self.source, element_start); return Err(self.emit_error(SyntaxErrorKind::ExpectCloseTag { tag_name: tag_name.into(), line, @@ -1293,7 +1280,7 @@ impl<'s> Parser<'s> { children.push(children_parser(self)?); } None => { - let (line, column) = self.pos_to_line_col(tag_start); + let (line, column) = helpers::pos_to_line_col(self.source, tag_start); return Err(self.emit_error(SyntaxErrorKind::ExpectJinjaBlockEnd { tag_name: tag_name.into(), line, From bc91cad29c0525bb1a64a93680b6164993ed73b1 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 12 May 2026 17:14:32 +0800 Subject: [PATCH 57/67] chore: optimize displaying errors --- dprint_plugin/examples/fmt_ext.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/dprint_plugin/examples/fmt_ext.rs b/dprint_plugin/examples/fmt_ext.rs index caf15b73..a4531f7a 100644 --- a/dprint_plugin/examples/fmt_ext.rs +++ b/dprint_plugin/examples/fmt_ext.rs @@ -1,6 +1,6 @@ use anyhow::Error; use dprint_core::configuration::GlobalConfiguration; -use markup_fmt::{config::FormatOptions, detect_language, format_text}; +use markup_fmt::{FormatError, config::FormatOptions, detect_language, format_text}; use std::{borrow::Cow, env, fs, io, path::Path}; fn main() { @@ -79,7 +79,18 @@ fn main() { Ok(Cow::from(code)) } }, - ) - .unwrap(); - print!("{formatted}"); + ); + match formatted { + Ok(formatted) => { + print!("{formatted}"); + } + Err(FormatError::Syntax(error)) => { + eprintln!("{error}"); + } + Err(FormatError::External(errors)) => { + errors.into_iter().for_each(|error| { + eprintln!("{error}"); + }); + } + } } From e88e271f47b3f1e13021eeddc24f61bb2e5cab54 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 15 May 2026 11:52:15 +0800 Subject: [PATCH 58/67] feat: replace external syntax error position manually (#223) --- Cargo.lock | 17 +- dprint_plugin/tests/integration.rs | 2 +- markup_fmt/Cargo.toml | 2 + markup_fmt/README.md | 11 +- markup_fmt/examples/fmt.rs | 6 +- markup_fmt/src/ctx.rs | 170 +++++++++---------- markup_fmt/src/error.rs | 16 +- markup_fmt/src/helpers.rs | 2 +- markup_fmt/src/lib.rs | 18 +- markup_fmt/src/printer.rs | 255 +++++++++++++++-------------- markup_fmt/tests/fmt.rs | 28 ++-- 11 files changed, 268 insertions(+), 259 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f577981..11fd798c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -622,6 +622,7 @@ dependencies = [ "insta", "itertools", "memchr", + "regex", "serde", "similar-asserts", "tiny_pretty", @@ -802,11 +803,23 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", diff --git a/dprint_plugin/tests/integration.rs b/dprint_plugin/tests/integration.rs index fc4ddf94..9024197a 100644 --- a/dprint_plugin/tests/integration.rs +++ b/dprint_plugin/tests/integration.rs @@ -10,7 +10,7 @@ use std::{borrow::Cow, fs, io, path::Path}; #[test] fn integration_with_dprint_ts_snapshot() { - fn format_with_dprint_ts(input: &str, path: &Path) -> Result> { + fn format_with_dprint_ts(input: &str, path: &Path) -> Result { let mut options = match fs::read_to_string(path.with_extension("toml")) { Ok(file) => toml::from_str::(&file).unwrap(), Err(e) if e.kind() == io::ErrorKind::NotFound => Default::default(), diff --git a/markup_fmt/Cargo.toml b/markup_fmt/Cargo.toml index 92f28b9f..c6297a86 100644 --- a/markup_fmt/Cargo.toml +++ b/markup_fmt/Cargo.toml @@ -10,9 +10,11 @@ exclude = ["/tests"] [dependencies] aho-corasick = "1.1" +anyhow = "1.0" css_dataset = { version = "0.4", default-features = false, features = ["tags"] } itertools = "0.14" memchr = "2.7" +regex = "1.12" serde = { version = "1.0", optional = true } tiny_pretty = { version = "0.2", features = ["unicode-width"] } diff --git a/markup_fmt/README.md b/markup_fmt/README.md index c8ce696e..1a432160 100644 --- a/markup_fmt/README.md +++ b/markup_fmt/README.md @@ -12,7 +12,7 @@ assert_eq!("
    \n", &format_text( "
    ", Language::Html, &options, - |code, _| Ok::<_, std::convert::Infallible>(code.into()), + |code, _| Ok(code.into()), ).unwrap()); ``` @@ -30,27 +30,26 @@ assert!(matches!( "
    ", Language::Html, &options, - |code, _| Ok::<_, std::convert::Infallible>(code.into()), + |code, _| Ok(code.into()), ).unwrap_err(), FormatError::Syntax(SyntaxError { .. }) )); ``` -External formatter can return [`Err`] as well. +External formatter can return `anyhow::Error`. This error will be aggregated and returned in [`FormatError::External`]: ```rust +use anyhow::Error; use markup_fmt::{config::FormatOptions, format_text, FormatError, Language}; -struct ExternalFormatterError; - let options = FormatOptions::default(); assert!(matches!( format_text( "", Language::Html, &options, - |_, _| Err(ExternalFormatterError), + |_, _| Err(Error::msg("external formatter error")), ).unwrap_err(), FormatError::External(errors) if !errors.is_empty() )); diff --git a/markup_fmt/examples/fmt.rs b/markup_fmt/examples/fmt.rs index a3dd4e7b..1d18a4c1 100644 --- a/markup_fmt/examples/fmt.rs +++ b/markup_fmt/examples/fmt.rs @@ -1,5 +1,5 @@ use markup_fmt::{config::FormatOptions, detect_language, format_text}; -use std::{convert::Infallible, env, fs, io}; +use std::{env, fs, io}; fn main() -> anyhow::Result<()> { let file_path = env::args().nth(1).unwrap(); @@ -16,9 +16,7 @@ fn main() -> anyhow::Result<()> { } }; - let formatted = format_text(&code, language, &options, |code, _| { - Ok::<_, Infallible>(code.into()) - })?; + let formatted = format_text(&code, language, &options, |code, _| Ok(code.into()))?; print!("{formatted}"); Ok(()) } diff --git a/markup_fmt/src/ctx.rs b/markup_fmt/src/ctx.rs index a6934861..6f1930e2 100644 --- a/markup_fmt/src/ctx.rs +++ b/markup_fmt/src/ctx.rs @@ -4,14 +4,20 @@ use crate::{ helpers, state::State, }; +use anyhow::Error; use memchr::memchr; -use std::borrow::Cow; +use regex::{Captures, Regex}; +use std::{borrow::Cow, sync::LazyLock}; const QUOTES: [&str; 3] = ["\"", "\"", "'"]; -pub(crate) struct Ctx<'b, E, F> +static RE_LINE_COLUMN: LazyLock = LazyLock::new(|| { + Regex::new(r"(?:[Ll]ine\s*(\d+),?\s*[Cc]ol(?:umn)?\s*(\d+))|:(\d+):(\d+)").unwrap() +}); + +pub(crate) struct Ctx<'b, F> where - F: for<'a> FnMut(&'a str, Hints<'b>) -> Result, E>, + F: for<'a> FnMut(&'a str, Hints<'b>) -> Result, Error>, { pub(crate) source: &'b str, pub(crate) language: Language, @@ -19,12 +25,12 @@ where pub(crate) print_width: usize, pub(crate) options: &'b LanguageOptions, pub(crate) external_formatter: F, - pub(crate) external_formatter_errors: Vec, + pub(crate) external_formatter_errors: Vec, } -impl<'b, E, F> Ctx<'b, E, F> +impl<'b, F> Ctx<'b, F> where - F: for<'a> FnMut(&'a str, Hints<'b>) -> Result, E>, + F: for<'a> FnMut(&'a str, Hints<'b>) -> Result, Error>, { pub(crate) fn script_indent(&self) -> bool { match self.language { @@ -135,30 +141,19 @@ where code: &str, attr: bool, start: usize, - ) -> Result { - if code.trim().is_empty() { + ) -> Result { + let code = code.trim_ascii(); + if code.is_empty() { Ok(String::new()) } else { // Trim original code before sending it to the external formatter. // This makes sure the code will be trimmed // though external formatter isn't available. - let preprocessed = code.trim_start(); - let will_add_brackets = - preprocessed.starts_with('{') || preprocessed.starts_with("..."); + let will_add_brackets = code.starts_with('{') || code.starts_with("..."); let wrapped = if will_add_brackets { - self.source - .get(0..start.saturating_sub(1)) - .unwrap_or_default() - .replace(|c: char| !c.is_ascii_whitespace(), " ") - + "[" - + code.trim() - + "]" + &format!("[{code}]") } else { - self.source - .get(0..start) - .unwrap_or_default() - .replace(|c: char| !c.is_ascii_whitespace(), " ") - + code + code }; let formatted = self.try_format_with_external_formatter( wrapped, @@ -168,11 +163,12 @@ where attr, ext: "tsx", }, + start, )?; let mut formatted = formatted.trim_matches(|c: char| c.is_ascii_whitespace() || c == ';'); - formatted = trim_delim(preprocessed, formatted, '[', ']'); - formatted = trim_delim(preprocessed, formatted, '(', ')'); + formatted = trim_delim(code, formatted, '[', ']'); + formatted = trim_delim(code, formatted, '(', ')'); if will_add_brackets { formatted = formatted.trim_ascii_end().trim_end_matches(','); } @@ -181,25 +177,20 @@ where } pub(crate) fn format_binding(&mut self, code: &str, start: usize) -> String { - if code.trim().is_empty() { + let code = code.trim_ascii(); + if code.is_empty() { String::new() } else { - let wrapped = self - .source - .get(0..start.saturating_sub(4)) - .unwrap_or_default() - .replace(|c: char| !c.is_ascii_whitespace(), " ") - + "let " - + code.trim() - + " = 0"; + let wrapped = format!("let {code} = 0"); let formatted = self.format_with_external_formatter( - wrapped, + &wrapped, Hints { print_width: self.print_width, indent_level: 0, attr: false, ext: "ts", }, + start, ); let formatted = formatted.trim_matches(|c: char| c.is_ascii_whitespace() || c == ';'); formatted @@ -211,25 +202,20 @@ where } pub(crate) fn format_type_params(&mut self, code: &str, start: usize) -> String { - if code.trim().is_empty() { + let code = code.trim_ascii(); + if code.is_empty() { String::new() } else { - let wrapped = self - .source - .get(0..start.saturating_sub(7)) - .unwrap_or_default() - .replace(|c: char| !c.is_ascii_whitespace(), " ") - + "type T<" - + code.trim() - + "> = 0"; + let wrapped = format!("type T<{code}> = 0"); let formatted = self.format_with_external_formatter( - wrapped, + &wrapped, Hints { print_width: self.print_width, indent_level: 0, attr: true, ext: "ts", }, + start, ); let formatted = formatted.trim_matches(|c: char| c.is_ascii_whitespace() || c == ';'); formatted @@ -243,18 +229,20 @@ where } pub(crate) fn format_stmt_header(&mut self, keyword: &str, code: &str) -> String { - if code.trim().is_empty() { + let code = code.trim_ascii(); + if code.is_empty() { String::new() } else { let wrapped = format!("{keyword} ({code}) {{}}"); let formatted = self.format_with_external_formatter( - wrapped, + &wrapped, Hints { print_width: self.print_width, indent_level: 0, attr: false, ext: "js", }, + 0, ); formatted .strip_prefix(keyword) @@ -290,19 +278,16 @@ where lang: &'b str, start: usize, state: &State, - ) -> Result, E> { + ) -> Result, Error> { self.try_format_with_external_formatter( - self.source - .get(0..start) - .unwrap_or_default() - .replace(|c: char| !c.is_ascii_whitespace(), " ") - + code, + code, Hints { print_width: self.print_width, indent_level: state.indent_level, attr: false, ext: lang, }, + start, ) } @@ -314,14 +299,7 @@ where state: &State, ) -> Cow<'a, str> { self.format_with_external_formatter( - "\n".repeat( - self.source - .get(0..start) - .unwrap_or_default() - .lines() - .count() - .saturating_sub(1), - ) + code, + code, Hints { print_width: self .print_width @@ -335,22 +313,20 @@ where attr: false, ext: if lang == "postcss" { "css" } else { lang }, }, + start, ) } pub(crate) fn format_style_attr(&mut self, code: &str, start: usize, state: &State) -> String { self.format_with_external_formatter( - self.source - .get(0..start) - .unwrap_or_default() - .replace(|c: char| !c.is_ascii_whitespace(), " ") - + code, + code, Hints { print_width: u16::MAX as usize, indent_level: state.indent_level, attr: true, ext: "css", }, + start, ) .trim() .to_owned() @@ -363,11 +339,7 @@ where state: &State, ) -> Cow<'a, str> { self.format_with_external_formatter( - self.source - .get(0..start) - .unwrap_or_default() - .replace(|c: char| !c.is_ascii_whitespace(), " ") - + code, + code, Hints { print_width: self .print_width @@ -381,6 +353,7 @@ where attr: false, ext: "json", }, + start, ) } @@ -392,11 +365,7 @@ where state: &State, ) -> String { self.format_with_external_formatter( - self.source - .get(0..start) - .unwrap_or_default() - .replace(|c: char| !c.is_ascii_whitespace(), " ") - + code, + code, Hints { print_width: self .print_width @@ -409,6 +378,7 @@ where "markup-fmt-jinja-stmt" }, }, + start, ) .trim_ascii() .to_owned() @@ -416,12 +386,12 @@ where fn format_with_external_formatter<'a>( &mut self, - code: String, + code: &'a str, hints: Hints<'b>, + start: usize, ) -> Cow<'a, str> { - match (self.external_formatter)(&code, hints) { - Ok(Cow::Owned(formatted)) => Cow::from(formatted), - Ok(Cow::Borrowed(..)) => Cow::from(code), + match self.try_format_with_external_formatter(code, hints, start) { + Ok(formatted) => formatted, Err(e) => { self.external_formatter_errors.push(e); code.into() @@ -431,13 +401,45 @@ where fn try_format_with_external_formatter<'a>( &mut self, - code: String, + code: &'a str, hints: Hints<'b>, - ) -> Result, E> { - match (self.external_formatter)(&code, hints) { + start: usize, + ) -> Result, Error> { + match (self.external_formatter)(code, hints) { Ok(Cow::Owned(formatted)) => Ok(Cow::from(formatted)), Ok(Cow::Borrowed(..)) => Ok(Cow::from(code)), - Err(e) => Err(e), + Err(e) => { + let msg = e.to_string(); + let (start_line, start_col) = helpers::pos_to_line_col(self.source, start); + let msg = RE_LINE_COLUMN + .replace_all(&msg, |captures: &Captures| { + captures + .get(1) + .or_else(|| captures.get(3)) + .zip(captures.get(2).or_else(|| captures.get(4))) + .and_then(|(line, col)| { + Some(( + msg.get(..line.start())?, + msg.get(line.range()) + .and_then(|line| line.parse::().ok())?, + msg.get(line.end()..col.start())?, + msg.get(col.range()) + .and_then(|col| col.parse::().ok())?, + msg.get(col.end()..)?, + )) + }) + .map(|(prefix, line, mid, col, suffix)| { + format!( + "{prefix}{}{mid}{}{suffix}", + (start_line + line).saturating_sub(1), + start_col + col + ) + }) + .unwrap_or_else(|| msg.clone()) + }) + .to_string(); + Err(Error::msg(msg)) + } } } } diff --git a/markup_fmt/src/error.rs b/markup_fmt/src/error.rs index 951f6f47..9cdecc56 100644 --- a/markup_fmt/src/error.rs +++ b/markup_fmt/src/error.rs @@ -1,4 +1,5 @@ -use std::{borrow::Cow, error::Error, fmt}; +use anyhow::Error; +use std::{borrow::Cow, fmt}; #[derive(Clone, Debug)] /// Syntax error when parsing tags, not `