@@ -3,6 +3,8 @@ use crate::extractor::bracket_stack::BracketStack;
33use crate :: extractor:: machine:: { Machine , MachineState } ;
44use crate :: extractor:: pre_processors:: pre_processor:: PreProcessor ;
55use crate :: extractor:: variant_machine:: VariantMachine ;
6+ use crate :: scanner:: pre_process_input;
7+ use bstr:: ByteVec ;
68
79#[ derive( Debug , Default ) ]
810pub struct Haml ;
@@ -14,8 +16,153 @@ impl PreProcessor for Haml {
1416 let mut cursor = cursor:: Cursor :: new ( content) ;
1517 let mut bracket_stack = BracketStack :: default ( ) ;
1618
19+ // Haml Comments: -#
20+ // https://haml.info/docs/yardoc/file.REFERENCE.html#ruby-evaluation
21+ //
22+ // > The hyphen followed immediately by the pound sign signifies a silent comment. Any text
23+ // > following this isn’t rendered in the resulting document at all.
24+ //
25+ // ```haml
26+ // %p foo
27+ // -# This is a comment
28+ // %p bar
29+ // ```
30+ //
31+ // > You can also nest text beneath a silent comment. None of this text will be rendered.
32+ //
33+ // ```haml
34+ // %p foo
35+ // -#
36+ // This won't be displayed
37+ // Nor will this
38+ // Nor will this.
39+ // %p bar
40+ // ```
41+ //
42+ // Ruby Evaluation
43+ // https://haml.info/docs/yardoc/file.REFERENCE.html#ruby-evaluation
44+ //
45+ // When any of the following characters are the first non-whitespace character on the line,
46+ // then the line is treated as Ruby code:
47+ //
48+ // - Inserting Ruby: =
49+ // https://haml.info/docs/yardoc/file.REFERENCE.html#inserting_ruby
50+ //
51+ // ```haml
52+ // %p
53+ // = ['hi', 'there', 'reader!'].join " "
54+ // = "yo"
55+ // ```
56+ //
57+ // - Running Ruby: -
58+ // https://haml.info/docs/yardoc/file.REFERENCE.html#running-ruby--
59+ //
60+ // ```haml
61+ // - foo = "hello"
62+ // - foo << " there"
63+ // - foo << " you!"
64+ // %p= foo
65+ // ```
66+ //
67+ // - Whitespace Preservation: ~
68+ // https://haml.info/docs/yardoc/file.REFERENCE.html#tilde
69+ //
70+ // > ~ works just like =, except that it runs Haml::Helpers.preserve on its input.
71+ //
72+ // ```haml
73+ // ~ "Foo\n<pre>Bar\nBaz</pre>"
74+ // ```
75+ //
76+ // Important note:
77+ //
78+ // > A line of Ruby code can be stretched over multiple lines as long as each line but the
79+ // > last ends with a comma.
80+ //
81+ // ```haml
82+ // - links = {:home => "/",
83+ // :docs => "/docs",
84+ // :about => "/about"}
85+ // ```
86+ //
87+ // Ruby Blocks:
88+ // https://haml.info/docs/yardoc/file.REFERENCE.html#ruby-blocks
89+ //
90+ // > Ruby blocks, like XHTML tags, don’t need to be explicitly closed in Haml. Rather,
91+ // > they’re automatically closed, based on indentation. A block begins whenever the
92+ // > indentation is increased after a Ruby evaluation command. It ends when the indentation
93+ // > decreases (as long as it’s not an else clause or something similar).
94+ //
95+ // ```haml
96+ // - (42...47).each do |i|
97+ // %p= i
98+ // %p See, I can count!
99+ // ```
100+ //
101+ let mut last_newline_position = 0 ;
102+
17103 while cursor. pos < len {
18104 match cursor. curr {
105+ // Escape the next character
106+ b'\\' => {
107+ cursor. advance_twice ( ) ;
108+ continue ;
109+ }
110+
111+ // Track the last newline position
112+ b'\n' => {
113+ last_newline_position = cursor. pos ;
114+ cursor. advance ( ) ;
115+ continue ;
116+ }
117+
118+ // Skip HAML comments. `-#`
119+ b'-' if cursor. input [ last_newline_position..cursor. pos ]
120+ . iter ( )
121+ . all ( u8:: is_ascii_whitespace)
122+ && matches ! ( cursor. next, b'#' ) =>
123+ {
124+ // Just consume the comment
125+ let updated_last_newline_position =
126+ self . skip_indented_block ( & mut cursor, last_newline_position) ;
127+
128+ // Override the last known newline position
129+ last_newline_position = updated_last_newline_position;
130+ }
131+
132+ // Skip HTML comments. `/`
133+ b'/' if cursor. input [ last_newline_position..cursor. pos ]
134+ . iter ( )
135+ . all ( u8:: is_ascii_whitespace) =>
136+ {
137+ // Just consume the comment
138+ let updated_last_newline_position =
139+ self . skip_indented_block ( & mut cursor, last_newline_position) ;
140+
141+ // Override the last known newline position
142+ last_newline_position = updated_last_newline_position;
143+ }
144+
145+ // Ruby evaluation
146+ b'-' | b'=' | b'~'
147+ if cursor. input [ last_newline_position..cursor. pos ]
148+ . iter ( )
149+ . all ( u8:: is_ascii_whitespace) =>
150+ {
151+ let mut start = cursor. pos ;
152+ let end = self . skip_indented_block ( & mut cursor, last_newline_position) ;
153+
154+ // Increment start with 1 character to skip the `=` or `-` character
155+ start += 1 ;
156+
157+ let ruby_code = & cursor. input [ start..end] ;
158+
159+ // Override the last known newline position
160+ last_newline_position = end;
161+
162+ let replaced = pre_process_input ( ruby_code, "rb" ) ;
163+ result. replace_range ( start..end, replaced) ;
164+ }
165+
19166 // Only replace `.` with a space if it's not surrounded by numbers. E.g.:
20167 //
21168 // ```diff
@@ -89,6 +236,107 @@ impl PreProcessor for Haml {
89236 }
90237}
91238
239+ impl Haml {
240+ fn skip_indented_block (
241+ & self ,
242+ cursor : & mut cursor:: Cursor ,
243+ last_known_newline_position : usize ,
244+ ) -> usize {
245+ let len = cursor. input . len ( ) ;
246+
247+ // Special case: if the first character of the block is `=`, then newlines are only allowed
248+ // _if_ the last character of the previous line is a comma `,`.
249+ //
250+ // https://haml.info/docs/yardoc/file.REFERENCE.html#inserting_ruby
251+ //
252+ // > A line of Ruby code can be stretched over multiple lines as long as each line but the
253+ // > last ends with a comma. For example:
254+ //
255+ // ```haml
256+ // = link_to_remote "Add to cart",
257+ // :url => { :action => "add", :id => product.id },
258+ // :update => { :success => "cart", :failure => "error" }
259+ // ```
260+ let evaluation_type = cursor. curr ;
261+
262+ let block_indentation_level = cursor
263+ . pos
264+ . saturating_sub ( last_known_newline_position)
265+ . saturating_sub ( 1 ) ; /* The newline itself */
266+
267+ let mut last_newline_position = last_known_newline_position;
268+
269+ // Consume until the end of the line first
270+ while cursor. pos < len && cursor. curr != b'\n' {
271+ cursor. advance ( ) ;
272+ }
273+
274+ // Block is already done, aka just a line
275+ if evaluation_type == b'=' && cursor. prev != b',' {
276+ return cursor. pos ;
277+ }
278+
279+ ' outer: while cursor. pos < len {
280+ match cursor. curr {
281+ // Escape the next character
282+ b'\\' => {
283+ cursor. advance_twice ( ) ;
284+ continue ;
285+ }
286+
287+ // Track the last newline position
288+ b'\n' => {
289+ last_newline_position = cursor. pos ;
290+
291+ // We are done with this block
292+ if evaluation_type == b'=' && cursor. prev != b',' {
293+ break ;
294+ }
295+
296+ cursor. advance ( ) ;
297+ continue ;
298+ }
299+
300+ // Skip whitespace and compute the indentation level
301+ x if x. is_ascii_whitespace ( ) => {
302+ // Find first non-whitespace character
303+ while cursor. pos < len && cursor. curr . is_ascii_whitespace ( ) {
304+ if cursor. curr == b'\n' {
305+ last_newline_position = cursor. pos ;
306+
307+ if evaluation_type == b'=' && cursor. prev != b',' {
308+ // We are done with this block
309+ break ' outer;
310+ }
311+ }
312+
313+ cursor. advance ( ) ;
314+ }
315+
316+ let indentation = cursor
317+ . pos
318+ . saturating_sub ( last_newline_position)
319+ . saturating_sub ( 1 ) ; /* The newline itself */
320+ if indentation < block_indentation_level {
321+ // We are done with this block
322+ break ;
323+ }
324+ }
325+
326+ // Not whitespace, end of block
327+ _ => break ,
328+ } ;
329+
330+ cursor. advance ( ) ;
331+ }
332+
333+ // Move the cursor to the last newline position
334+ cursor. move_to ( last_newline_position) ;
335+
336+ last_newline_position
337+ }
338+ }
339+
92340#[ cfg( test) ]
93341mod tests {
94342 use super :: Haml ;
@@ -173,10 +421,18 @@ mod tests {
173421
174422 // https://github.com/tailwindlabs/tailwindcss/pull/17051#issuecomment-2711181352
175423 #[ test]
176- fn test_haml_full_file ( ) {
177- let processed = Haml . process ( include_bytes ! ( "./test-fixtures/haml/src-1.haml" ) ) ;
178- let actual = std:: str:: from_utf8 ( & processed) . unwrap ( ) ;
179- let expected = include_str ! ( "./test-fixtures/haml/dst-1.haml" ) ;
424+ fn test_haml_full_file_17051 ( ) {
425+ let actual = Haml :: extract_annotated ( include_bytes ! ( "./test-fixtures/haml/src-17051.haml" ) ) ;
426+ let expected = include_str ! ( "./test-fixtures/haml/dst-17051.haml" ) ;
427+
428+ assert_eq ! ( actual, expected) ;
429+ }
430+
431+ // https://github.com/tailwindlabs/tailwindcss/issues/17813
432+ #[ test]
433+ fn test_haml_full_file_17813 ( ) {
434+ let actual = Haml :: extract_annotated ( include_bytes ! ( "./test-fixtures/haml/src-17813.haml" ) ) ;
435+ let expected = include_str ! ( "./test-fixtures/haml/dst-17813.haml" ) ;
180436
181437 assert_eq ! ( actual, expected) ;
182438 }
0 commit comments