@@ -94,6 +94,23 @@ const OCAML_ATOM_NODES: [&str; 6] = [
9494 "attribute_id" ,
9595] ;
9696
97+ const TS_CSS_INJECTION_QUERY : & str = r#"
98+ (_
99+ ; Capture the 'callee' or 'left' side.
100+ ; We accept any named node here to act as the anchor.
101+ (_) @callee
102+
103+ ; Capture the template string contents.
104+ (template_string) @contents
105+
106+ ; Predicate: The 'callee' text must start with 'styled' or 'css'.
107+ ; This matches "styled.div", "styled(C)", and even "styled.div<{}>"
108+ ; (which might be parsed as a binary expression 'styled.div < {}' or
109+ ; a call expression depending on the grammar version).
110+ (#match? @callee "^(styled|css)")
111+ )
112+ "# ;
113+
97114pub ( crate ) fn from_language ( language : guess:: Language ) -> TreeSitterConfig {
98115 use guess:: Language :: * ;
99116 match language {
@@ -590,7 +607,10 @@ pub(crate) fn from_language(language: guess::Language) -> TreeSitterConfig {
590607 ] ,
591608 highlight_query : ts:: Query :: new ( & language, tree_sitter_javascript:: HIGHLIGHT_QUERY )
592609 . unwrap ( ) ,
593- sub_languages : vec ! [ ] ,
610+ sub_languages : vec ! [ TreeSitterSubLanguage {
611+ query: ts:: Query :: new( & language, TS_CSS_INJECTION_QUERY ) . unwrap( ) ,
612+ parse_as: Css ,
613+ } ] ,
594614 }
595615 }
596616 Json => {
@@ -1053,7 +1073,10 @@ pub(crate) fn from_language(language: guess::Language) -> TreeSitterConfig {
10531073 atom_nodes : [ "string" , "template_string" ] . into_iter ( ) . collect ( ) ,
10541074 delimiter_tokens : vec ! [ ( "{" , "}" ) , ( "(" , ")" ) , ( "[" , "]" ) , ( "<" , ">" ) ] ,
10551075 highlight_query : ts:: Query :: new ( & language, & highlight_query) . unwrap ( ) ,
1056- sub_languages : vec ! [ ] ,
1076+ sub_languages : vec ! [ TreeSitterSubLanguage {
1077+ query: ts:: Query :: new( & language, TS_CSS_INJECTION_QUERY ) . unwrap( ) ,
1078+ parse_as: Css ,
1079+ } ] ,
10571080 }
10581081 }
10591082 TypeScript => {
@@ -1070,7 +1093,10 @@ pub(crate) fn from_language(language: guess::Language) -> TreeSitterConfig {
10701093 . collect ( ) ,
10711094 delimiter_tokens : vec ! [ ( "{" , "}" ) , ( "(" , ")" ) , ( "[" , "]" ) , ( "<" , ">" ) ] ,
10721095 highlight_query : ts:: Query :: new ( & language, & highlight_query) . unwrap ( ) ,
1073- sub_languages : vec ! [ ] ,
1096+ sub_languages : vec ! [ TreeSitterSubLanguage {
1097+ query: ts:: Query :: new( & language, TS_CSS_INJECTION_QUERY ) . unwrap( ) ,
1098+ parse_as: Css ,
1099+ } ] ,
10741100 }
10751101 }
10761102 Xml => {
@@ -1198,8 +1224,39 @@ pub(crate) fn parse_subtrees(
11981224 let mut query_matches =
11991225 query_cursor. matches ( & language. query , tree. root_node ( ) , src. as_bytes ( ) ) ;
12001226
1227+ let content_idx = language
1228+ . query
1229+ . capture_index_for_name ( "contents" )
1230+ . unwrap_or ( 0 ) ;
1231+
12011232 while let Some ( m) = query_matches. next ( ) {
1202- let node = m. nodes_for_capture_index ( 0 ) . next ( ) . unwrap ( ) ;
1233+ let node = match m. nodes_for_capture_index ( content_idx) . next ( ) {
1234+ None => continue ,
1235+ Some ( node) => node,
1236+ } ;
1237+
1238+ let mut range = node. range ( ) ;
1239+ match ( language. parse_as , node. grammar_name ( ) ) {
1240+ ( guess:: Language :: Css , "template_string" ) => {
1241+ // If this is a template string (starts/ends with backtick), shrink the range.
1242+ // We check the text to be safe, or just assume based on the node kind.
1243+ let node_text = & src[ node. start_byte ( ) ..node. end_byte ( ) ] ;
1244+ if node_text. starts_with ( '`' )
1245+ && node_text. ends_with ( '`' )
1246+ && node_text. len ( ) >= 2
1247+ {
1248+ range. start_byte += 1 ;
1249+ range. end_byte -= 1 ;
1250+
1251+ // We also need to update start_point and end_point for Tree-sitter to be happy.
1252+ // Since we know a backtick is 1 column wide and doesn't span lines:
1253+ range. start_point . column += 1 ;
1254+ range. end_point . column -= 1 ;
1255+ }
1256+ }
1257+ _ => { }
1258+ } ;
1259+
12031260 if node. byte_range ( ) . is_empty ( ) {
12041261 continue ;
12051262 }
@@ -1210,7 +1267,7 @@ pub(crate) fn parse_subtrees(
12101267 . set_language ( & subconfig. language )
12111268 . expect ( "Incompatible tree-sitter version" ) ;
12121269 parser
1213- . set_included_ranges ( & [ node . range ( ) ] )
1270+ . set_included_ranges ( & [ range] )
12141271 . expect ( "Incompatible tree-sitter version" ) ;
12151272
12161273 let tree = parser. parse ( src, None ) . unwrap ( ) ;
@@ -1833,8 +1890,12 @@ fn atom_from_cursor<'a>(
18331890
18341891#[ cfg( test) ]
18351892mod tests {
1893+ use std:: collections:: VecDeque ;
1894+
18361895 use strum:: IntoEnumIterator as _;
18371896
1897+ use crate :: parse:: syntax:: SyntaxTreeDisplay ;
1898+
18381899 use super :: * ;
18391900
18401901 /// Simple smoke test for tree-sitter parsing. Having a test also
@@ -1880,6 +1941,128 @@ mod tests {
18801941 } ;
18811942 }
18821943
1944+ fn assert_contains_atoms < ' a > ( nodes : & [ & ' a Syntax < ' a > ] , expected_sequence : Vec < Vec < & str > > ) {
1945+ let mut to_search = VecDeque :: from ( nodes. to_vec ( ) ) ;
1946+ let mut expected_iter = expected_sequence. into_iter ( ) ;
1947+ let mut current_expected = expected_iter. next ( ) ;
1948+
1949+ while let Some ( node) = to_search. pop_front ( ) {
1950+ if let Some ( expected) = & current_expected {
1951+ match node {
1952+ Syntax :: List { children, .. } => {
1953+ // Extract just the normal atoms from this list to see if they match the line
1954+ let atom_texts: Vec < & str > = children
1955+ . iter ( )
1956+ . filter_map ( |child| match child {
1957+ Syntax :: Atom { content, .. } => Some ( content. as_str ( ) ) ,
1958+ _ => None ,
1959+ } )
1960+ . collect ( ) ;
1961+
1962+ // If this list matches the current expected line, advance expectation
1963+ if !atom_texts. is_empty ( ) && atom_texts == * expected {
1964+ current_expected = expected_iter. next ( ) ;
1965+ }
1966+
1967+ for child in children. iter ( ) . rev ( ) {
1968+ to_search. push_front ( child) ;
1969+ }
1970+ }
1971+ _ => { }
1972+ }
1973+ } else {
1974+ // All expectations met
1975+ return ;
1976+ }
1977+ }
1978+
1979+ if let Some ( remaining) = current_expected {
1980+ panic ! (
1981+ "Could not find all atom sequences. \n Missing: {:?}\n Debug Tree:\n {}" ,
1982+ remaining,
1983+ SyntaxTreeDisplay :: from( nodes. to_vec( ) )
1984+ ) ;
1985+ }
1986+ }
1987+
1988+ #[ test]
1989+ fn test_typescript_css_injection_table ( ) {
1990+ let arena = Arena :: new ( ) ;
1991+ let configs = vec ! [
1992+ from_language( guess:: Language :: TypeScript ) ,
1993+ from_language( guess:: Language :: TypeScriptTsx ) ,
1994+ from_language( guess:: Language :: JavaScript ) ,
1995+ from_language( guess:: Language :: JavascriptJsx ) ,
1996+ ] ;
1997+
1998+ let cases = vec ! [
1999+ // Case 1: Standard styled.button
2000+ (
2001+ r#"
2002+ const Button = styled.button`
2003+ background: #BF4F74;
2004+ border-radius: 3px;
2005+ border: none;
2006+ color: white;
2007+ `
2008+ "# ,
2009+ vec![
2010+ vec![ "background" , ":" , "#BF4F74" , ";" ] ,
2011+ vec![ "border-radius" , ":" , "3px" , ";" ] ,
2012+ vec![ "border" , ":" , "none" , ";" ] ,
2013+ vec![ "color" , ":" , "white" , ";" ] ,
2014+ ] ,
2015+ ) ,
2016+ // Case 2: Component wrapping styled(C)
2017+ (
2018+ r#"
2019+ const Button = styled(OtherButton)`
2020+ color: white;
2021+ `
2022+ "# ,
2023+ vec![ vec![ "color" , ":" , "white" , ";" ] ] ,
2024+ ) ,
2025+ // Case 3: Helper css`...`
2026+ (
2027+ r#"
2028+ const Button = css`
2029+ color: white;
2030+ `
2031+ "# ,
2032+ vec![ vec![ "color" , ":" , "white" , ";" ] ] ,
2033+ ) ,
2034+ // Case 4: Nested Interpolation
2035+ (
2036+ r#"
2037+ const Button = styled.button`
2038+ color: white;
2039+ ${props => props.$withSeparator && css`padding-top: 22px;`}
2040+ `
2041+ "# ,
2042+ vec![ vec![ "color" , ":" , "white" , ";" ] ] ,
2043+ ) ,
2044+ // Case 5: Generics
2045+ (
2046+ r#"
2047+ export const Button = styled.button<{}>` color: white; `;
2048+ "# ,
2049+ vec![ vec![ "color" , ":" , "white" , ";" ] ] ,
2050+ ) ,
2051+ // Case 6: Multiline Edge Case
2052+ (
2053+ "\n const X = styled.div`\n color: white;\n `" ,
2054+ vec![ vec![ "color" , ":" , "white" , ";" ] ] ,
2055+ ) ,
2056+ ] ;
2057+
2058+ for config in configs {
2059+ for ( src, expected_atoms) in cases. iter ( ) {
2060+ let nodes = parse ( & arena, src, & config, false ) ;
2061+ assert_contains_atoms ( & nodes, expected_atoms. clone ( ) ) ;
2062+ }
2063+ }
2064+ }
2065+
18832066 /// Ensure that we don't crash when loading any of the
18842067 /// configs. This can happen on bad highlighting/foo.scm files.
18852068 #[ test]
0 commit comments