1+ # frozen_string_literal: true
2+
13class 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 ?/
623
724 attr_reader :keep_these_class_names
825
@@ -12,11 +29,15 @@ def purge(input, keeping_class_names_from_files:)
1229 end
1330
1431 def extract_class_names ( string )
15- string . scan ( CLASS_NAME_PATTERN ) . flatten . uniq . sort
32+ string . scan ( CLASS_NAME_PATTERN ) . uniq . sort!
1633 end
1734
1835 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' )
2041 end
2142 end
2243
@@ -25,40 +46,79 @@ def initialize(keep_these_class_names)
2546 end
2647
2748 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"
4858 end
4959
50- separated_without_empty_lines ( output )
60+ conveyor . output
5161 end
5262
5363 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
5979 end
6080
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
63123 end
64124end
0 commit comments