1
+ # frozen_string_literal: true
2
+
1
3
class Tailwindcss ::Purger
2
- CLASS_NAME_PATTERN = /([:A-Za-z0-9_-]+[\. \\ \/ :A-Za-z0-9_-]*)/
3
- OPENING_SELECTOR_PATTERN = /\. .*\{ /
4
- CLOSING_SELECTOR_PATTERN = /\s *\} /
5
- NEWLINE = "\n "
4
+ CLASS_NAME_PATTERN = /[:A-Za-z0-9_-]+[\. ]*[\\ \/ :A-Za-z0-9_-]*/
5
+
6
+ CLASS_BREAK = /(?![-_a-z0-9\\ ])/i # `\b` for CSS classes
7
+
8
+ COMMENT = /#{ Regexp . escape "/*" } .*?#{ Regexp . escape "*/" } /m
9
+ COMMENTS_AND_BLANK_LINES = /\A (?:^#{ COMMENT } ?[ \t ]*(?:\n |\z )|[ \t ]*#{ COMMENT } )+/
10
+
11
+ AT_RULE = /@[^{]+/
12
+ CLASSLESS_SELECTOR_GROUP = /[^.{]+/
13
+ CLASSLESS_BEGINNING_OF_BLOCK = /\A \s *(?:#{ AT_RULE } |#{ CLASSLESS_SELECTOR_GROUP } )\{ \n ?/
14
+
15
+ SELECTOR_GROUP = /[^{]+/
16
+ BEGINNING_OF_BLOCK = /\A #{ SELECTOR_GROUP } \{ \n ?/
17
+
18
+ PROPERTY_NAME = /[-_a-z0-9]+/i
19
+ PROPERTY_VALUE = /(?:[^;]|;\S )+/
20
+ PROPERTIES = /\A (?:\s *#{ PROPERTY_NAME } :#{ PROPERTY_VALUE } ;\n ?)+/
21
+
22
+ END_OF_BLOCK = /\A \s *\} \n ?/
6
23
7
24
attr_reader :keep_these_class_names
8
25
@@ -12,11 +29,11 @@ def purge(input, keeping_class_names_from_files:)
12
29
end
13
30
14
31
def extract_class_names ( string )
15
- string . scan ( CLASS_NAME_PATTERN ) . flatten . uniq . sort
32
+ string . scan ( CLASS_NAME_PATTERN ) . uniq . sort!
16
33
end
17
34
18
35
def extract_class_names_from ( files )
19
- Array ( files ) . flat_map { |file | extract_class_names ( file . read ) } . uniq . sort
36
+ Array ( files ) . flat_map { |file | extract_class_names ( file . read ) } . uniq . sort!
20
37
end
21
38
end
22
39
@@ -25,40 +42,81 @@ def initialize(keep_these_class_names)
25
42
end
26
43
27
44
def purge ( input )
28
- inside_kept_selector = inside_ignored_selector = false
29
- output = [ ]
30
-
31
- input . split ( NEWLINE ) . each do |line |
32
- case
33
- when inside_kept_selector
34
- output << line
35
- inside_kept_selector = false if line =~ CLOSING_SELECTOR_PATTERN
36
- when inside_ignored_selector
37
- inside_ignored_selector = false if line =~ CLOSING_SELECTOR_PATTERN
38
- when line =~ OPENING_SELECTOR_PATTERN
39
- if keep_these_class_names . include? class_name_in ( line )
40
- output << line
41
- inside_kept_selector = true
42
- else
43
- inside_ignored_selector = true
44
- end
45
- else
46
- output << line
47
- end
45
+ conveyor = Conveyor . new ( input )
46
+
47
+ until conveyor . done?
48
+ conveyor . discard ( COMMENTS_AND_BLANK_LINES ) \
49
+ or conveyor . keep_if ( PROPERTIES ) { conveyor . staged_output . last != "" } \
50
+ or conveyor . keep_if ( END_OF_BLOCK ) { not conveyor . staged_output . pop } \
51
+ or conveyor . stage_for_output ( CLASSLESS_BEGINNING_OF_BLOCK ) \
52
+ or conveyor . stage_for_output ( BEGINNING_OF_BLOCK ) { |match | purge_beginning_of_block ( match . to_s ) } \
53
+ or raise "infinite loop"
48
54
end
49
55
50
- separated_without_empty_lines ( output )
56
+ conveyor . output
51
57
end
52
58
53
59
private
54
- def class_name_in ( line )
55
- CLASS_NAME_PATTERN . match ( line ) [ 1 ]
56
- . remove ( " \\ " )
57
- . remove ( /:(focus|hover)(-within)?/ )
58
- . remove ( "::placeholder" ) . remove ( "::-moz-placeholder" ) . remove ( ":-ms-input-placeholder" )
60
+ def keep_these_selectors_pattern
61
+ @keep_these_selectors_pattern ||= begin
62
+ classes = @keep_these_class_names . join ( "|" ) . gsub ( %r"[:./]" ) { | c | Regexp . escape " \\ #{ c } " }
63
+ /(?: \A |,)[^.,{]*(?:[.](?: #{ classes } ) #{ CLASS_BREAK } [^.,{]*)*(?=[,{])/
64
+ end
59
65
end
60
66
61
- def separated_without_empty_lines ( output )
62
- output . reject { |line | line . strip . empty? } . join ( NEWLINE )
67
+ def purge_beginning_of_block ( string )
68
+ purged = string . scan ( keep_these_selectors_pattern ) . join
69
+ unless purged . empty?
70
+ purged . sub! ( /\A ,\s */ , "" )
71
+ purged . rstrip!
72
+ purged << " {\n "
73
+ end
74
+ purged
75
+ end
76
+
77
+ class Conveyor
78
+ attr_reader :output , :staged_output
79
+
80
+ def initialize ( input , output = +"" )
81
+ @input = input
82
+ @output = output
83
+ @staged_output = [ ]
84
+ end
85
+
86
+ def consume ( pattern )
87
+ match = pattern . match ( @input )
88
+ @input = match . post_match if match
89
+ match
90
+ end
91
+ alias :discard :consume
92
+
93
+ def stage_for_output ( pattern )
94
+ if match = consume ( pattern )
95
+ string = block_given? ? ( yield match ) : match . to_s
96
+ @staged_output << string
97
+ string
98
+ end
99
+ end
100
+
101
+ def append_to_output ( pattern )
102
+ if match = consume ( pattern )
103
+ string = block_given? ? ( yield match ) : match . to_s
104
+ @output << @staged_output . shift until @staged_output . empty?
105
+ @output << string
106
+ string
107
+ end
108
+ end
109
+ alias :keep :append_to_output
110
+
111
+ def append_to_output_if ( pattern )
112
+ append_to_output ( pattern ) do |match |
113
+ ( yield match ) ? match . to_s : ( break "" )
114
+ end
115
+ end
116
+ alias :keep_if :append_to_output_if
117
+
118
+ def done?
119
+ @input . empty?
120
+ end
63
121
end
64
122
end
0 commit comments