Class: CssTidy::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/modules/css_parser.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeParser

setup the class vars used by Tidy



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/modules/css_parser.rb', line 19

def initialize

	# temporary array to hold data during development
	@stylesheet = CssTidy::StyleSheet.new

	# the raw, unprocessed css
	@raw_css = ''

	# the string that is being processed
	@css = ''

	# the current position in the string
	@index = 0

	# the current parser context. i.e where we are in the CSS
	@context = [NONE, IN_SELECTOR]

	# the current line number
	@line_number = 1
end

Instance Attribute Details

#cssObject

these are used to poke values in for testing instance methods



16
17
18
# File 'lib/modules/css_parser.rb', line 16

def css
  @css
end

#indexObject

these are used to poke values in for testing instance methods



16
17
18
# File 'lib/modules/css_parser.rb', line 16

def index
  @index
end

#sheetObject

these are used to poke values in for testing instance methods



16
17
18
# File 'lib/modules/css_parser.rb', line 16

def sheet
  @sheet
end

Instance Method Details

#current_charObject



323
324
325
# File 'lib/modules/css_parser.rb', line 323

def current_char
	@css[@index,1]
end

#is_at_rule?(text) ⇒ Boolean

Returns:

  • (Boolean)


435
436
437
# File 'lib/modules/css_parser.rb', line 435

def is_at_rule?(text)
    #if($this->selector{0} == '@' && isset($at_rules[substr($this->selector,1)]) && $at_rules[substr($this->selector,1)] == 'iv')
end

#is_char_ctype?(ctype, char) ⇒ Boolean

Returns:

  • (Boolean)


414
415
416
417
418
419
420
421
422
423
# File 'lib/modules/css_parser.rb', line 414

def is_char_ctype?(ctype, char)
	case ctype
	when :space
		char =~ / |\t|\f|\v|\n|\r/
	when :xdigit # hexidecimal
		char =~ /[0-9a-f]/i
	when :alpha
		char =~ /[A-Za-z]/
	end
end

#is_char_escaped?(char) ⇒ Boolean

Returns:

  • (Boolean)


373
374
375
376
377
378
379
380
381
# File 'lib/modules/css_parser.rb', line 373

def is_char_escaped?(char)
	# cannot backtrack before index '1' (would be -1, or the end of the string)
	if @index > 0
		if char === '\\'
			return true
		end
	end
	false
end

#is_char_token?(char) ⇒ Boolean

Returns:

  • (Boolean)


364
365
366
# File 'lib/modules/css_parser.rb', line 364

def is_char_token?(char)
	TOKENS.include?(char)
end

#is_comment?Boolean

Returns:

  • (Boolean)


384
385
386
387
388
389
390
391
392
# File 'lib/modules/css_parser.rb', line 384

def is_comment?
	# cannot look beyond the end of the string
	if @index < @css.length
		if @css[@index, 2] == '/*'
			return true
		end
	end
	false
end

#is_comment_end?Boolean

Returns:

  • (Boolean)


394
395
396
397
398
399
400
401
402
# File 'lib/modules/css_parser.rb', line 394

def is_comment_end?
	# cannot look beyond the end of the string
	if @index < @css.length
		if @css[@index, 2] == '*/'
			return true
		end
	end
	false
end

#is_css_whitespace?(char) ⇒ Boolean

Returns:

  • (Boolean)


353
354
355
# File 'lib/modules/css_parser.rb', line 353

def is_css_whitespace?(char)
	WHITESPACE.include?(char)
end

#is_ctype?(ctype, offset = 0) ⇒ Boolean

Returns:

  • (Boolean)


408
409
410
411
412
# File 'lib/modules/css_parser.rb', line 408

def is_ctype?(ctype, offset=0)
	if @index < @css.length
		is_char_ctype?(ctype, @css[@index+offset,1])
	end
end

#is_current_char?(char, offset = 0) ⇒ Boolean

any sort of character - use for readability

Returns:

  • (Boolean)


426
427
428
429
430
431
432
433
# File 'lib/modules/css_parser.rb', line 426

def is_current_char?(char,offset=0)
	case char.class.to_s
	when 'String'
		@css[@index+offset,1] == char
	when 'Array'
		char.include?(@css[@index+offset,1])
	end
end

#is_escaped?(offset = 0) ⇒ Boolean

Checks if a character is escaped (and returns true if it is)

Returns:

  • (Boolean)


369
370
371
# File 'lib/modules/css_parser.rb', line 369

def is_escaped?(offset=0)
	is_char_escaped?(@css[@index+offset-1,1])
end

#is_newline?Boolean

Returns:

  • (Boolean)


404
405
406
# File 'lib/modules/css_parser.rb', line 404

def is_newline?
	@css[@index,1] =~ /\n|\r/
end

#is_property_valid?(property) ⇒ Boolean

Returns:

  • (Boolean)


349
350
351
# File 'lib/modules/css_parser.rb', line 349

def is_property_valid?(property)
	PROPERTIES.has_key?(property)
end

#is_token?(offset = 0) ⇒ Boolean

These functions all test the character at the current index location

Returns:

  • (Boolean)


360
361
362
# File 'lib/modules/css_parser.rb', line 360

def is_token?(offset=0)
	is_char_token?(@css[@index+offset,1])
end

#parse(css) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/modules/css_parser.rb', line 40

def parse(css)
	css_length = css.length
	@css = css.clone

	# vars used in processing of sheets
	current_at_block = ''
	invalid_at = false
	invalid_at_name = ''

	current_selector = ''
	current_property = ''
	current_ruleset = CssTidy::RuleSet.new

	current_value = ''
	sub_value = ''
	sub_value_array = []

	current_string = ''
	string_char = ''
	str_in_str = false

	current_comment = ''

	while @index < css_length

		if is_newline?
			@line_number += 1
		end
		case @context.last
		when IN_AT_BLOCK
			if is_token?
				# current_at_block empty? allows comment inside of selectors to pass
				if is_comment? && current_at_block.strip.empty?
					@context << IN_COMMENT
					@index += 1 # move past '*'
				elsif is_current_char? '{'
					@context << IN_SELECTOR
				elsif is_current_char? ','
					current_at_block = current_at_block.strip + ','
				elsif is_current_char? ['(',')',':','/','*','!','\\']
					# catch media queries and escapes
         	current_at_block << current_char
				end # of is_comment
			else # not token
				if(! ( (is_char_ctype?(:space, current_at_block[-1,1]) || is_char_token?(current_at_block[-1,1]) && current_at_block[-1,1] == ',') && is_ctype?(:space) ))
         	current_at_block << current_char
         end
			end

		when IN_SELECTOR
        if is_token?
				# current_selector empty? allows comment inside of selectors to pass
				if is_comment? && current_selector.strip.empty?
					@context << IN_COMMENT
					@index += 1
      		elsif is_current_char?('@') && current_selector.strip.empty?
           # Check for at-rule
           invalid_at = true
					AT_RULES.each do |name, type|
						size_of_property = name.length
						# look ahead for the name
						property_to_find = (@css[@index+1,size_of_property]).strip.downcase
						if name == property_to_find
							if type == IN_AT_BLOCK
								current_at_block = '@' + name
							else
								current_selector = '@' + name
							end
							@context << type
							@index += size_of_property
							invalid_at = false
						end
           end

					if invalid_at
             current_selector = '@'
             invalid_at_name = ''
						puts "invalid At rule"
             # for($j = $i+1; $j < $size; ++$j)
             # {
             #     if(!ctype_alpha($string{$j}))
             #     {
             #         break;
             #     }
             #     $invalid_at_name .= $string{$j};
             # }
             # $this->log('Invalid @-rule: '.$invalid_at_name.' (removed)','Warning');
           end
         elsif is_current_char?('"') || is_current_char?("'")
					@context << IN_STRING
					current_string = current_char
					string_char = current_char
         elsif invalid_at && is_current_char?(';')
					invalid_at = false
					@context << IN_SELECTOR
         elsif is_current_char?('{')
					@context << IN_PROPERTY
         elsif is_current_char?('}')
					current_at_block = ''
					@stylesheet.end_at_block
					current_selector = ''
					# when there is a new selector we save the last set
					@stylesheet << current_ruleset unless current_ruleset.empty?
					# and start a new one
					current_ruleset = CssTidy::RuleSet.new
         elsif is_current_char?([',','\\'])
					current_selector = current_selector.strip + current_char
         #remove unnecessary universal selector,  FS#147
				#elseif ! (is_current_char?('*') && @in_array($string{$i+1}, array('.', '#', '[', ':'))))
				else
					current_selector << current_char
				end
        else # not is_token
				last_position = current_selector.length - 1
				if( last_position == -1 || ! ( (is_char_ctype?(:space, current_selector[last_position,1]) || is_char_token?(current_selector[last_position,1]) && current_selector[last_position,1] == ',') && is_ctype?(:space) ))
         	current_selector << current_char
         end
        end

		when IN_PROPERTY
			if is_token?
				if (is_current_char?(':') || is_current_char?('=')) && ! current_property.empty?
					@context << IN_VALUE
         elsif is_comment? && current_property.empty?
					@context << IN_COMMENT
					@index += 1 # move past '*'
         elsif is_current_char?('}')
					@context << IN_SELECTOR
					invalid_at = false
					current_property = ''
					current_selector = ''
					# when there is a new selector we save the last set
					@stylesheet << current_ruleset unless current_ruleset.empty?
					# and start a new one
					current_ruleset = CssTidy::RuleSet.new
         elsif is_current_char?(';')
					current_property = ''
         elsif is_current_char?(['*','\\']) # allow star hack and \ hack for properties
					current_property << current_char
         end
        elsif ! is_ctype?(:space)
				current_property << current_char
        end

		when IN_VALUE
        property_next = is_newline? && property_is_next? || @index == css_length-1
        if is_token? || property_next
         if is_comment?
					@context << IN_COMMENT
					@index += 1
         elsif is_current_char?('"') || is_current_char?("'") || is_current_char?('(')
					current_string = current_char
					string_char = is_current_char?('(') ? ')' : current_char
           @context << IN_STRING
         elsif is_current_char?([',','\\'])
					sub_value = sub_value.strip + current_char
         elsif is_current_char?(';') || property_next
					if current_selector[0,1] == '@' && AT_RULES.has_key?(current_selector[1..-1]) && AT_RULES[current_selector[1..-1]] == IN_VALUE
						sub_value_array << sub_value.strip

						@context << IN_SELECTOR
						case current_selector
                 when '@charset'
									unless (@stylesheet.charset = sub_value_array[0])
										puts "extra charset"
									end
                 when '@namespace'
									#$this->namespace = implode(' ',$this->sub_value_arr);
                 when '@import'
									@stylesheet << CssTidy::Import.new(sub_value_array.join(' '))
						end

             sub_value_array = []
             sub_value = ''
             current_selector = ''
           else
           	@context << IN_PROPERTY
          	end
         elsif ! is_current_char?('}')
           sub_value << current_char
         end

         if (is_current_char?('}') || is_current_char?(';') || property_next) && ! current_selector.empty?
           unless current_at_block.empty?
						@stylesheet << CssTidy::MediaSet.new(current_at_block.strip)
						current_at_block = ''
           end

           if ! sub_value.strip.empty?
             sub_value_array << sub_value.strip
             sub_value = ''
           end

           current_value = sub_value_array.join(' ')

           valid = is_property_valid?(current_property)
           if (! invalid_at || valid)
						current_ruleset.add_rule({:selector => current_selector.strip, :declarations => "#{current_property}:#{current_value}" })
           end

           current_property = ''
					sub_value_array = []
           current_value = ''
         end

         if is_current_char?('}')
					@context << IN_SELECTOR
					invalid_at = false
					current_selector = ''
					# when there is a new selector we save the last set
					@stylesheet << current_ruleset unless current_ruleset.empty?
					# and start a new one
					current_ruleset = CssTidy::RuleSet.new
         end
        elsif ! property_next
         sub_value << current_char

         if is_ctype?(:space)
					if ! sub_value.strip.empty?
             sub_value_array << sub_value.strip
             sub_value = ''
           end
         end
        end

		when IN_STRING
			if string_char === ')' && (is_current_char?('"') || is_current_char?("'")) && ! str_in_str && ! is_escaped?
				str_in_str = true
			elsif string_char === ')' && (is_current_char?('"') || is_current_char?("'")) && str_in_str && ! is_escaped?
				str_in_str = false
			end
			temp_add = current_char	# // ...and no not-escaped backslash at the previous position

			if is_newline? && !is_current_char?('\\',-1) && ! is_escaped?(-1)
				temp_add = "\\A "
         	#$this->log('Fixed incorrect newline in string','Warning');
			end

       if !(string_char === ')' && is_css_whitespace?(current_char) && !str_in_str)
				current_string << temp_add
       end

        if is_current_char?(string_char) && !is_escaped? && !str_in_str
				@context.pop

				if is_css_whitespace?(current_string) && current_property != 'content'
					if (!quoted_string)
						if (string_char === '"' || string_char === '\'')
							# Temporarily disable this optimization to avoid problems with @charset rule, quote properties, and some attribute selectors...
							# Attribute selectors fixed, added quotes to @chartset, no problems with properties detected. Enabled
							#current_string = current_string.slice($this->cur_string, 1, -1);
						elsif (current_string > 3) && (current_string[1,1] === '"' || current_string[1,1] === '\'')
							#current_string = current_string + substr($this->cur_string, 2, -2) . substr($this->cur_string, -1);
						end
					else
						quoted_string = false
					end
				end

				if @context[-1] === IN_VALUE # from in value?
             sub_value << current_string
          elsif @context[-1] === IN_SELECTOR
           current_selector << current_string;
          end
			end

		when IN_COMMENT
			if is_comment_end?
				@context.pop # go back to previous context
				@index += 1 # skip the '/'
				@stylesheet << CssTidy::Comment.new(current_comment)
				current_comment = ''
        else
				current_comment << current_char
        end

		end
		@index += 1
	end

	@stylesheet
end

#property_is_next?Boolean

Checks if the next word in a string from after current index is a CSS property

Returns:

  • (Boolean)


328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/modules/css_parser.rb', line 328

def property_is_next?
	pos = @css.index(':', @index+1)

	if ! pos
		return false
	end

	# get the length until just before the ':'
	size_of_property = pos - @index - 1

	# extract the name of the property
	property_to_find = (@css[@index+1,size_of_property]).strip.downcase

	if PROPERTIES.has_key?(property_to_find)
		#$this->log('Added semicolon to the end of declaration','Warning');
		return true
	else
		return false
	end
end