@@ -3,6 +3,8 @@ use crate::extractor::bracket_stack::BracketStack;
3
3
use crate :: extractor:: machine:: { Machine , MachineState } ;
4
4
use crate :: extractor:: pre_processors:: pre_processor:: PreProcessor ;
5
5
use crate :: extractor:: variant_machine:: VariantMachine ;
6
+ use crate :: scanner:: pre_process_input;
7
+ use bstr:: ByteVec ;
6
8
7
9
#[ derive( Debug , Default ) ]
8
10
pub struct Haml ;
@@ -14,8 +16,153 @@ impl PreProcessor for Haml {
14
16
let mut cursor = cursor:: Cursor :: new ( content) ;
15
17
let mut bracket_stack = BracketStack :: default ( ) ;
16
18
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
+
17
103
while cursor. pos < len {
18
104
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
+
19
166
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
20
167
//
21
168
// ```diff
@@ -89,6 +236,107 @@ impl PreProcessor for Haml {
89
236
}
90
237
}
91
238
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
+
92
340
#[ cfg( test) ]
93
341
mod tests {
94
342
use super :: Haml ;
@@ -173,10 +421,18 @@ mod tests {
173
421
174
422
// https://github.com/tailwindlabs/tailwindcss/pull/17051#issuecomment-2711181352
175
423
#[ 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" ) ;
180
436
181
437
assert_eq ! ( actual, expected) ;
182
438
}
0 commit comments