11use rustc_hash:: FxHashMap ;
22use serde_json:: { Value , json} ;
33
4+ use oxc_allocator:: Allocator ;
5+ use oxc_ast:: ast:: TemplateElement ;
6+ use oxc_ast_visit:: Visit ;
47use oxc_formatter:: {
58 BestFittingElement , DedentMode , FormatElement , GroupId , LineMode , PrintMode , Tag ,
9+ get_parse_options,
610} ;
11+ use oxc_parser:: Parser ;
12+ use oxc_span:: { Span , SourceType } ;
713
814// TODO: Currently, we build a Prettier Doc tree using `serde_json::Value`,
915// then serialize it to a string.
@@ -13,8 +19,14 @@ use oxc_formatter::{
1319// - caching interned elements
1420// - reusing constant Doc structures like `{type: "line", hard: true}`
1521
16- /// Splits a printed string by newlines and joins with Prettier `hardline` docs.
17- pub fn printed_string_to_hardline_doc ( text : & str ) -> Value {
22+ /// Splits a printed string by newlines and joins with Prettier docs.
23+ /// Newlines inside template literal text use `literalline` (ignores parent indent),
24+ /// all other newlines use `hardline` (respects parent indent).
25+ ///
26+ /// This is critical for `vueIndentScriptAndStyle: true` where Prettier wraps
27+ /// the `textToDoc()` result with `indent()`. Without `literalline`, template
28+ /// literal content gains extra indentation on each format pass (non-idempotent).
29+ pub fn printed_string_to_doc_with_template_literals ( text : & str , source_ext : & str ) -> Value {
1830 // `lines()` will remove trailing newlines, but it is fine.
1931 // For js-in-xxx fragments, do not need to preserve trailing newlines.
2032 let lines: Vec < & str > = text. lines ( ) . collect ( ) ;
@@ -23,20 +35,94 @@ pub fn printed_string_to_hardline_doc(text: &str) -> Value {
2335 return Value :: String ( text. to_string ( ) ) ;
2436 }
2537
38+ // Collect template literal element byte ranges from the formatted code.
39+ let template_literal_ranges = collect_template_element_spans ( text, source_ext) ;
40+
41+ // Build the Doc, choosing `literalline` vs `hardline` per newline position.
2642 let mut parts: Vec < Value > = Vec :: with_capacity ( lines. len ( ) * 3 ) ;
43+ let mut byte_offset: usize = 0 ;
44+
2745 for ( i, line) in lines. iter ( ) . enumerate ( ) {
2846 if 0 < i {
29- // hardline = [{ type: "line", hard: true } + break-parent]
30- // https://github.com/prettier/prettier/blob/90983f40dce5e20beea4e5618b5e0426a6a7f4f0/src/document/builders/line.js#L27
31- parts. push ( json ! ( { "type" : "line" , "hard" : true } ) ) ;
32- parts. push ( json ! ( { "type" : "break-parent" } ) ) ;
47+ // The newline character is at `byte_offset - 1` (we advanced past it).
48+ // But we compute it as: the byte just after the previous line's content.
49+ let newline_pos = byte_offset;
50+ if is_in_template_literal ( newline_pos, & template_literal_ranges) {
51+ // literalline = { type: "line", hard: true, literal: true } + break-parent
52+ push_literal_line ( & mut parts) ;
53+ } else {
54+ // hardline = { type: "line", hard: true } + break-parent
55+ parts. push ( json ! ( { "type" : "line" , "hard" : true } ) ) ;
56+ parts. push ( json ! ( { "type" : "break-parent" } ) ) ;
57+ }
58+ // Account for the newline character(s)
59+ // Handle both \n and \r\n
60+ if byte_offset < text. len ( ) && text. as_bytes ( ) [ byte_offset] == b'\r' {
61+ byte_offset += 1 ;
62+ }
63+ if byte_offset < text. len ( ) && text. as_bytes ( ) [ byte_offset] == b'\n' {
64+ byte_offset += 1 ;
65+ }
3366 }
3467 parts. push ( Value :: String ( ( * line) . to_string ( ) ) ) ;
68+ byte_offset += line. len ( ) ;
3569 }
3670
3771 normalize_array ( parts)
3872}
3973
74+ /// Check if a byte offset falls within any template literal element span.
75+ fn is_in_template_literal ( pos : usize , ranges : & [ Span ] ) -> bool {
76+ // Ranges are sorted, so binary search for efficiency.
77+ ranges. binary_search_by ( |span| {
78+ let start = span. start as usize ;
79+ let end = span. end as usize ;
80+ if pos < start {
81+ std:: cmp:: Ordering :: Greater
82+ } else if pos >= end {
83+ std:: cmp:: Ordering :: Less
84+ } else {
85+ std:: cmp:: Ordering :: Equal
86+ }
87+ } ) . is_ok ( )
88+ }
89+
90+ /// Parse the formatted code and collect all `TemplateElement` spans.
91+ /// These spans cover the raw text portions of template literals
92+ /// (between `\`` and `${` or `}` and `\``).
93+ fn collect_template_element_spans ( text : & str , source_ext : & str ) -> Vec < Span > {
94+ let Ok ( source_type) = SourceType :: from_extension ( source_ext) else {
95+ return Vec :: new ( ) ;
96+ } ;
97+
98+ let allocator = Allocator :: default ( ) ;
99+ let ret = Parser :: new ( & allocator, text, source_type)
100+ . with_options ( get_parse_options ( ) )
101+ . parse ( ) ;
102+
103+ // If parsing fails, fall back to all-hardline (no literallines).
104+ if !ret. errors . is_empty ( ) {
105+ return Vec :: new ( ) ;
106+ }
107+
108+ let mut visitor = TemplateElementCollector { spans : Vec :: new ( ) } ;
109+ visitor. visit_program ( & ret. program ) ;
110+ // Sort by start position for binary search.
111+ visitor. spans . sort_unstable_by_key ( |s| s. start ) ;
112+ visitor. spans
113+ }
114+
115+ /// AST visitor that collects spans of `TemplateElement` nodes.
116+ struct TemplateElementCollector {
117+ spans : Vec < Span > ,
118+ }
119+
120+ impl < ' a > Visit < ' a > for TemplateElementCollector {
121+ fn visit_template_element ( & mut self , elem : & TemplateElement < ' a > ) {
122+ self . spans . push ( elem. span ) ;
123+ }
124+ }
125+
40126/// Converts `oxc_formatter` IR (`FormatElement` slice) into Prettier Doc.
41127///
42128/// This is used for js-in-xxx fragment formatting
0 commit comments