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 class selectors
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,15 @@ 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!
37
+ end
38
+
39
+ def escape_class_selector ( class_name )
40
+ class_name . gsub ( /\A \d |[^-_a-z0-9]/ , '\\\\\0' )
20
41
end
21
42
end
22
43
@@ -25,40 +46,79 @@ def initialize(keep_these_class_names)
25
46
end
26
47
27
48
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
49
+ conveyor = Conveyor . new ( input )
50
+
51
+ until conveyor . done?
52
+ conveyor . discard ( COMMENTS_AND_BLANK_LINES ) \
53
+ or conveyor . conditionally_keep ( PROPERTIES ) { conveyor . staged_output . last != "" } \
54
+ or conveyor . conditionally_keep ( END_OF_BLOCK ) { not conveyor . staged_output . pop } \
55
+ or conveyor . stage_output ( CLASSLESS_BEGINNING_OF_BLOCK ) \
56
+ or conveyor . stage_output ( BEGINNING_OF_BLOCK ) { |match | purge_beginning_of_block ( match . to_s ) } \
57
+ or raise "infinite loop"
48
58
end
49
59
50
- separated_without_empty_lines ( output )
60
+ conveyor . output
51
61
end
52
62
53
63
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" )
64
+ def keep_these_selectors_pattern
65
+ @keep_these_selectors_pattern ||= begin
66
+ escaped_classes = @keep_these_class_names . map { |name | Regexp . escape self . class . escape_class_selector ( name ) }
67
+ /(?:\A |,)[^.,{]*(?:[.](?:#{ escaped_classes . join ( "|" ) } )#{ CLASS_BREAK } [^.,{]*)*(?=[,{])/
68
+ end
69
+ end
70
+
71
+ def purge_beginning_of_block ( string )
72
+ purged = string . scan ( keep_these_selectors_pattern ) . join
73
+ unless purged . empty?
74
+ purged . sub! ( /\A ,\s */ , "" )
75
+ purged . rstrip!
76
+ purged << " {\n "
77
+ end
78
+ purged
59
79
end
60
80
61
- def separated_without_empty_lines ( output )
62
- output . reject { |line | line . strip . empty? } . join ( NEWLINE )
81
+ class Conveyor
82
+ attr_reader :output , :staged_output
83
+
84
+ def initialize ( input , output = +"" )
85
+ @input = input
86
+ @output = output
87
+ @staged_output = [ ]
88
+ end
89
+
90
+ def consume ( pattern )
91
+ match = pattern . match ( @input )
92
+ @input = match . post_match if match
93
+ match
94
+ end
95
+ alias :discard :consume
96
+
97
+ def stage_output ( pattern )
98
+ if match = consume ( pattern )
99
+ string = block_given? ? ( yield match ) : match . to_s
100
+ @staged_output << string
101
+ string
102
+ end
103
+ end
104
+
105
+ def keep ( pattern )
106
+ if match = consume ( pattern )
107
+ string = block_given? ? ( yield match ) : match . to_s
108
+ @output << @staged_output . shift until @staged_output . empty?
109
+ @output << string
110
+ string
111
+ end
112
+ end
113
+
114
+ def conditionally_keep ( pattern )
115
+ keep ( pattern ) do |match |
116
+ ( yield match ) ? match . to_s : ( break "" )
117
+ end
118
+ end
119
+
120
+ def done?
121
+ @input . empty?
122
+ end
63
123
end
64
124
end
0 commit comments