@@ -5,41 +5,64 @@ use serde_json::Value;
55
66use oxc_allocator:: { Allocator , StringBuilder } ;
77use oxc_formatter:: {
8- Align , Condition , DedentMode , FormatElement , Group , GroupId , GroupMode , IndentWidth , LineMode ,
9- PrintMode , Tag , TextWidth , UniqueGroupIdBuilder ,
8+ Align , Condition , DedentMode , EmbeddedDocResult , FormatElement , Group , GroupId , GroupMode ,
9+ IndentWidth , LineMode , PrintMode , Tag , TextWidth , UniqueGroupIdBuilder ,
1010} ;
1111
1212/// Marker string used to represent `-Infinity` in JSON.
1313/// JS side replaces `-Infinity` with this string before `JSON.stringify()`.
1414/// See `src-js/lib/apis.ts` for details.
1515const NEGATIVE_INFINITY_MARKER : & str = "__NEGATIVE_INFINITY__" ;
1616
17- /// Converts a Prettier Doc JSON value into a flat `Vec<FormatElement<'a>>`,
18- /// with template-specific text escaping applied as post-processing.
19- pub fn to_format_elements_for_template < ' a > (
20- doc : & Value ,
17+ /// Converts Prettier Doc JSON strings into an [`EmbeddedDocResult`].
18+ ///
19+ /// Handles both language-specific processing:
20+ /// - CSS (`tagged-css`, `styled-jsx`, `angular-styles`):
21+ /// merges consecutive Text nodes, counts placeholders → [`EmbeddedDocResult::DocWithPlaceholders`]
22+ /// - Others (GraphQL, etc.):
23+ /// converts each doc independently → [`EmbeddedDocResult::MultipleDocs`]
24+ pub fn to_embedded_doc_result < ' a > (
25+ language : & str ,
26+ doc_json_strs : Vec < String > ,
27+ allocator : & ' a Allocator ,
28+ group_id_builder : & UniqueGroupIdBuilder ,
29+ ) -> Result < EmbeddedDocResult < ' a > , String > {
30+ match language {
31+ "tagged-css" | "styled-jsx" | "angular-styles" => {
32+ let doc_json_str = doc_json_strs
33+ . into_iter ( )
34+ . next ( )
35+ . ok_or_else ( || "Expected exactly one Doc JSON for CSS" . to_string ( ) ) ?;
36+ let mut ir = parse_and_convert ( & doc_json_str, allocator, group_id_builder) ?;
37+ let count = postprocess ( & mut ir, allocator) ;
38+ Ok ( EmbeddedDocResult :: DocWithPlaceholders ( ir, count) )
39+ }
40+ "tagged-graphql" => {
41+ let irs = doc_json_strs
42+ . into_iter ( )
43+ . map ( |doc_json_str| {
44+ let mut ir = parse_and_convert ( & doc_json_str, allocator, group_id_builder) ?;
45+ let _ = postprocess ( & mut ir, allocator) ;
46+ Ok ( ir)
47+ } )
48+ . collect :: < Result < Vec < _ > , String > > ( ) ?;
49+ Ok ( EmbeddedDocResult :: MultipleDocs ( irs) )
50+ }
51+ _ => unreachable ! ( "Unsupported embedded_doc language: {language}" ) ,
52+ }
53+ }
54+
55+ /// JSON parse → Doc → FormatElement IR.
56+ fn parse_and_convert < ' a > (
57+ doc_json_str : & str ,
2158 allocator : & ' a Allocator ,
2259 group_id_builder : & UniqueGroupIdBuilder ,
2360) -> Result < Vec < FormatElement < ' a > > , String > {
61+ let doc_json: Value =
62+ serde_json:: from_str ( doc_json_str) . map_err ( |e| format ! ( "Failed to parse Doc JSON: {e}" ) ) ?;
2463 let mut ctx = FmtCtx :: new ( allocator, group_id_builder) ;
2564 let mut out = vec ! [ ] ;
26- convert_doc ( doc, & mut out, & mut ctx) ?;
27-
28- postprocess ( & mut out, |fe| {
29- if let FormatElement :: Text { text, width } = fe {
30- // Some characters (e.g. backticks) should be escaped in template literals
31- let escaped = escape_template_characters ( text, allocator) ;
32- if !std:: ptr:: eq ( * text, escaped) {
33- * text = escaped;
34- // NOTE: `IndentWidth` only affects tab character width calculation.
35- // If a `Doc = string` node contained `\t` (e.g. inside a string literal like `"\t"`?),
36- // the width could be miscalculated when `options.indent_width` != 2.
37- // However, the default value is sufficient in practice.
38- * width = TextWidth :: from_text ( escaped, IndentWidth :: default ( ) ) ;
39- }
40- }
41- } ) ;
42-
65+ convert_doc ( & doc_json, & mut out, & mut ctx) ?;
4366 Ok ( out)
4467}
4568
@@ -333,29 +356,31 @@ fn extract_group_id(
333356
334357// ---
335358
336- /// Post-process `FormatElement`s:
337- /// - strip trailing hardline
338- /// - collapse consecutive hardlines into empty lines
359+ /// Post-process FormatElements in a single compaction pass:
360+ /// - strip trailing hardline (useless for embedded parts)
361+ /// - collapse double-hardlines `[Hard, ExpandParent, Hard, ExpandParent]` → `[Empty, ExpandParent]`
362+ /// - merge consecutive Text nodes (SCSS emits split strings like `"@"` + `"prettier-placeholder-0-id"`)
363+ /// - escape template characters (`\`, `` ` ``, `${`)
364+ /// - count `@prettier-placeholder-N-id` patterns
339365///
340- /// And apply a per-element callback for custom transformations.
341- fn postprocess < ' a > ( ir : & mut Vec < FormatElement < ' a > > , mut each : impl FnMut ( & mut FormatElement < ' a > ) ) {
342- // Strip trailing `hardline` pattern from FormatElement output.
343- // Trailing line is useless for embedded parts.
366+ /// Returns the placeholder count (0 for non-CSS languages).
367+ fn postprocess < ' a > ( ir : & mut Vec < FormatElement < ' a > > , allocator : & ' a Allocator ) -> usize {
368+ const PREFIX : & str = "@prettier-placeholder-" ;
369+ const SUFFIX : & str = "-id" ;
370+
371+ // Strip trailing hardline
344372 if ir. len ( ) >= 2
345373 && matches ! ( ir[ ir. len( ) - 1 ] , FormatElement :: ExpandParent )
346374 && matches ! ( ir[ ir. len( ) - 2 ] , FormatElement :: Line ( LineMode :: Hard ) )
347375 {
348376 ir. truncate ( ir. len ( ) - 2 ) ;
349377 }
350378
351- // Collapse consecutive `[Line(Hard), ExpandParent, Line(Hard), ExpandParent]` into `[Line(Empty), ExpandParent]`.
352- //
353- // In Prettier's Doc format, a blank line is represented as `hardline,
354- // hardline` which expands to `[Line(Hard), ExpandParent, Line(Hard), ExpandParent]`.
355- // However, `oxc_formatter`'s printer needs `Line(Empty)` instead.
379+ let mut placeholder_count = 0 ;
356380 let mut write = 0 ;
357381 let mut read = 0 ;
358382 while read < ir. len ( ) {
383+ // Collapse double-hardline → empty line
359384 if read + 3 < ir. len ( )
360385 && matches ! ( ir[ read] , FormatElement :: Line ( LineMode :: Hard ) )
361386 && matches ! ( ir[ read + 1 ] , FormatElement :: ExpandParent )
@@ -366,17 +391,57 @@ fn postprocess<'a>(ir: &mut Vec<FormatElement<'a>>, mut each: impl FnMut(&mut Fo
366391 ir[ write + 1 ] = FormatElement :: ExpandParent ;
367392 write += 2 ;
368393 read += 4 ;
394+ } else if matches ! ( ir[ read] , FormatElement :: Text { .. } ) {
395+ // Merge consecutive Text nodes + escape + count placeholders
396+ let run_start = read;
397+ read += 1 ;
398+ while read < ir. len ( ) && matches ! ( ir[ read] , FormatElement :: Text { .. } ) {
399+ read += 1 ;
400+ }
401+
402+ let escaped = if read - run_start == 1 {
403+ let FormatElement :: Text { text, .. } = & ir[ run_start] else { unreachable ! ( ) } ;
404+ escape_template_characters ( text, allocator)
405+ } else {
406+ let mut sb = StringBuilder :: new_in ( allocator) ;
407+ for element in & ir[ run_start..read] {
408+ if let FormatElement :: Text { text, .. } = element {
409+ sb. push_str ( text) ;
410+ }
411+ }
412+ escape_template_characters ( sb. into_str ( ) , allocator)
413+ } ;
414+ let width = TextWidth :: from_text ( escaped, IndentWidth :: default ( ) ) ;
415+ ir[ write] = FormatElement :: Text { text : escaped, width } ;
416+ write += 1 ;
417+
418+ // Count placeholders
419+ let mut remaining = escaped;
420+ while let Some ( start) = remaining. find ( PREFIX ) {
421+ let after_prefix = & remaining[ start + PREFIX . len ( ) ..] ;
422+ let digit_end = after_prefix
423+ . bytes ( )
424+ . position ( |b| !b. is_ascii_digit ( ) )
425+ . unwrap_or ( after_prefix. len ( ) ) ;
426+ if digit_end > 0
427+ && let Some ( rest) = after_prefix[ digit_end..] . strip_prefix ( SUFFIX )
428+ {
429+ placeholder_count += 1 ;
430+ remaining = rest;
431+ continue ;
432+ }
433+ remaining = & remaining[ start + PREFIX . len ( ) ..] ;
434+ }
369435 } else {
370436 if write != read {
371437 ir[ write] = ir[ read] . clone ( ) ;
372438 }
373- each ( & mut ir[ write] ) ;
374439 write += 1 ;
375440 read += 1 ;
376441 }
377442 }
378-
379443 ir. truncate ( write) ;
444+ placeholder_count
380445}
381446
382447/// Escape characters that would break template literal syntax.
0 commit comments