css_parser 1.7.1 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2dafcbcaaa6e1e5ecb45b092b2d997c799d02c1c06d90130989b45cc085452d
4
- data.tar.gz: 1b5cfa1908355aa798b96ed3234e00ea26d3f4a8f36076a2f6ccdec840dfaa71
3
+ metadata.gz: 47d696725a77d514c0a1ca529dc18e39d41bdd4106a786f21219f14c2ccf43f5
4
+ data.tar.gz: eb6a39b541cfa6bd8c4d66fc4e0adb06310922794b47e582989aba6dd8f5729b
5
5
  SHA512:
6
- metadata.gz: a589901eab26c8b78d8b0c5eae5224f249ff6e30caa9021c41d583b5a530b4b926f7d3f57cefad6c7b340c9ea925cc44e6b7b86321f6cecfd27684921822b594
7
- data.tar.gz: 60882d77d15847bab9aa70ebcfc788cf959580e61577e372badf1677aacee6958eae54e91e6db8ea6063a1cc9b627525a704adb13040b7be601608b69627b0d7
6
+ metadata.gz: db5d0a6cf245796841621194c360c31f5f349596fa00a755152bd109a50707ff5dc301a47601fb09400fa00479e2319a9321f3657c9eb0d5a11b4db7c5d8b048
7
+ data.tar.gz: 7dd50eff88bc656f81928098d736fb7c657ebe6ced0328523cee6e3e778f5b2e7d285311436be6ffd210f8b122467f99dfad739828d43530db41e60a74694b25
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module CssParser
3
4
  # Exception class used for any errors encountered while downloading remote files.
4
5
  class RemoteFileError < IOError; end
@@ -15,13 +16,12 @@ module CssParser
15
16
  # [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
16
17
  # [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
17
18
  class Parser
18
- USER_AGENT = "Ruby CSS Parser/#{CssParser::VERSION} (https://github.com/premailer/css_parser)"
19
-
20
- STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
21
- STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
19
+ USER_AGENT = "Ruby CSS Parser/#{CssParser::VERSION} (https://github.com/premailer/css_parser)"
20
+ STRIP_CSS_COMMENTS_RX = %r{/\*.*?\*/}m.freeze
21
+ STRIP_HTML_COMMENTS_RX = /<!--|-->/m.freeze
22
22
 
23
23
  # Initial parsing
24
- RE_AT_IMPORT_RULE = /\@import\s*(?:url\s*)?(?:\()?(?:\s*)["']?([^'"\s\)]*)["']?\)?([\w\s\,^\]\(\)]*)\)?[;\n]?/
24
+ RE_AT_IMPORT_RULE = /@import\s*(?:url\s*)?(?:\()?(?:\s*)["']?([^'"\s)]*)["']?\)?([\w\s,^\]()]*)\)?[;\n]?/.freeze
25
25
 
26
26
  MAX_REDIRECTS = 3
27
27
 
@@ -35,10 +35,14 @@ module CssParser
35
35
  class << self; attr_reader :folded_declaration_cache; end
36
36
 
37
37
  def initialize(options = {})
38
- @options = {:absolute_paths => false,
39
- :import => true,
40
- :io_exceptions => true,
41
- :capture_offsets => false}.merge(options)
38
+ @options = {
39
+ absolute_paths: false,
40
+ import: true,
41
+ io_exceptions: true,
42
+ rule_set_exceptions: true,
43
+ capture_offsets: false,
44
+ user_agent: USER_AGENT
45
+ }.merge(options)
42
46
 
43
47
  # array of RuleSets
44
48
  @rules = []
@@ -70,21 +74,20 @@ module CssParser
70
74
  # Returns an array of declarations.
71
75
  def find_by_selector(selector, media_types = :all)
72
76
  out = []
73
- each_selector(media_types) do |sel, dec, spec|
77
+ each_selector(media_types) do |sel, dec, _spec|
74
78
  out << dec if sel.strip == selector.strip
75
79
  end
76
80
  out
77
81
  end
78
- alias_method :[], :find_by_selector
82
+ alias [] find_by_selector
79
83
 
80
84
  # Finds the rule sets that match the given selectors
81
85
  def find_rule_sets(selectors, media_types = :all)
82
86
  rule_sets = []
83
87
 
84
88
  selectors.each do |selector|
85
- selector.gsub!(/\s+/, ' ')
86
- selector.strip!
87
- each_rule_set(media_types) do |rule_set, media_type|
89
+ selector = selector.gsub(/\s+/, ' ').strip
90
+ each_rule_set(media_types) do |rule_set, _media_type|
88
91
  if !rule_sets.member?(rule_set) && rule_set.selectors.member?(selector)
89
92
  rule_sets << rule_set
90
93
  end
@@ -115,9 +118,9 @@ module CssParser
115
118
  # parser = CssParser::Parser.new
116
119
  # parser.add_block!(css)
117
120
  def add_block!(block, options = {})
118
- options = {:base_uri => nil, :base_dir => nil, :charset => nil, :media_types => :all, :only_media_types => :all}.merge(options)
119
- options[:media_types] = [options[:media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
120
- options[:only_media_types] = [options[:only_media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
121
+ options = {base_uri: nil, base_dir: nil, charset: nil, media_types: :all, only_media_types: :all}.merge(options)
122
+ options[:media_types] = [options[:media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
123
+ options[:only_media_types] = [options[:only_media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
121
124
 
122
125
  block = cleanup_block(block, options)
123
126
 
@@ -129,19 +132,19 @@ module CssParser
129
132
  if @options[:import]
130
133
  block.scan(RE_AT_IMPORT_RULE).each do |import_rule|
131
134
  media_types = []
132
- if media_string = import_rule[-1]
133
- media_string.split(/[,]/).each do |t|
135
+ if (media_string = import_rule[-1])
136
+ media_string.split(/,/).each do |t|
134
137
  media_types << CssParser.sanitize_media_query(t) unless t.empty?
135
138
  end
136
139
  else
137
140
  media_types = [:all]
138
141
  end
139
142
 
140
- next unless options[:only_media_types].include?(:all) or media_types.length < 1 or (media_types & options[:only_media_types]).length > 0
143
+ next unless options[:only_media_types].include?(:all) or media_types.empty? or !(media_types & options[:only_media_types]).empty?
141
144
 
142
145
  import_path = import_rule[0].to_s.gsub(/['"]*/, '').strip
143
146
 
144
- import_options = { :media_types => media_types }
147
+ import_options = {media_types: media_types}
145
148
  import_options[:capture_offsets] = true if options[:capture_offsets]
146
149
 
147
150
  if options[:base_uri]
@@ -167,6 +170,8 @@ module CssParser
167
170
  def add_rule!(selectors, declarations, media_types = :all)
168
171
  rule_set = RuleSet.new(selectors, declarations)
169
172
  add_rule_set!(rule_set, media_types)
173
+ rescue ArgumentError => e
174
+ raise e if @options[:rule_set_exceptions]
170
175
  end
171
176
 
172
177
  # Add a CSS rule by setting the +selectors+, +declarations+, +filename+, +offset+ and +media_types+.
@@ -183,21 +188,21 @@ module CssParser
183
188
  #
184
189
  # +media_types+ can be a symbol or an array of symbols.
185
190
  def add_rule_set!(ruleset, media_types = :all)
186
- raise ArgumentError unless ruleset.kind_of?(CssParser::RuleSet)
191
+ raise ArgumentError unless ruleset.is_a?(CssParser::RuleSet)
187
192
 
188
- media_types = [media_types] unless Array === media_types
189
- media_types = media_types.flat_map { |mt| CssParser.sanitize_media_query(mt)}
193
+ media_types = [media_types] unless media_types.is_a?(Array)
194
+ media_types = media_types.flat_map { |mt| CssParser.sanitize_media_query(mt) }
190
195
 
191
- @rules << {:media_types => media_types, :rules => ruleset}
196
+ @rules << {media_types: media_types, rules: ruleset}
192
197
  end
193
198
 
194
199
  # Remove a CssParser RuleSet object.
195
200
  #
196
201
  # +media_types+ can be a symbol or an array of symbols.
197
202
  def remove_rule_set!(ruleset, media_types = :all)
198
- raise ArgumentError unless ruleset.kind_of?(CssParser::RuleSet)
203
+ raise ArgumentError unless ruleset.is_a?(CssParser::RuleSet)
199
204
 
200
- media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
205
+ media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
201
206
 
202
207
  @rules.reject! do |rule|
203
208
  rule[:media_types] == media_types && rule[:rules].to_s == ruleset.to_s
@@ -209,7 +214,7 @@ module CssParser
209
214
  # +media_types+ can be a symbol or an array of symbols.
210
215
  def each_rule_set(media_types = :all) # :yields: rule_set, media_types
211
216
  media_types = [:all] if media_types.nil?
212
- media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
217
+ media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
213
218
 
214
219
  @rules.each do |block|
215
220
  if media_types.include?(:all) or block[:media_types].any? { |mt| media_types.include?(mt) }
@@ -222,7 +227,7 @@ module CssParser
222
227
  def to_h(which_media = :all)
223
228
  out = {}
224
229
  styles_by_media_types = {}
225
- each_selector(which_media) do |selectors, declarations, specificity, media_types|
230
+ each_selector(which_media) do |selectors, declarations, _specificity, media_types|
226
231
  media_types.each do |media_type|
227
232
  styles_by_media_types[media_type] ||= []
228
233
  styles_by_media_types[media_type] << [selectors, declarations]
@@ -244,7 +249,7 @@ module CssParser
244
249
  # +media_types+ can be a symbol or an array of symbols.
245
250
  # See RuleSet#each_selector for +options+.
246
251
  def each_selector(all_media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types
247
- return to_enum(:each_selector) unless block_given?
252
+ return to_enum(__method__, all_media_types, options) unless block_given?
248
253
 
249
254
  each_rule_set(all_media_types) do |rule_set, media_types|
250
255
  rule_set.each_selector(options) do |selectors, declarations, specificity|
@@ -255,9 +260,10 @@ module CssParser
255
260
 
256
261
  # Output all CSS rules as a single stylesheet.
257
262
  def to_s(which_media = :all)
258
- out = String.new
263
+ out = []
259
264
  styles_by_media_types = {}
260
- each_selector(which_media) do |selectors, declarations, specificity, media_types|
265
+
266
+ each_selector(which_media) do |selectors, declarations, _specificity, media_types|
261
267
  media_types.each do |media_type|
262
268
  styles_by_media_types[media_type] ||= []
263
269
  styles_by_media_types[media_type] << [selectors, declarations]
@@ -266,20 +272,21 @@ module CssParser
266
272
 
267
273
  styles_by_media_types.each_pair do |media_type, media_styles|
268
274
  media_block = (media_type != :all)
269
- out << "@media #{media_type} {\n" if media_block
275
+ out << "@media #{media_type} {" if media_block
270
276
 
271
277
  media_styles.each do |media_style|
272
278
  if media_block
273
- out << " #{media_style[0]} {\n #{media_style[1]}\n }\n"
279
+ out.push(" #{media_style[0]} {\n #{media_style[1]}\n }")
274
280
  else
275
- out << "#{media_style[0]} {\n#{media_style[1]}\n}\n"
281
+ out.push("#{media_style[0]} {\n#{media_style[1]}\n}")
276
282
  end
277
283
  end
278
284
 
279
- out << "}\n" if media_block
285
+ out << '}' if media_block
280
286
  end
281
287
 
282
- out
288
+ out << ''
289
+ out.join("\n")
283
290
  end
284
291
 
285
292
  # A hash of { :media_query => rule_sets }
@@ -287,7 +294,7 @@ module CssParser
287
294
  rules_by_media = {}
288
295
  @rules.each do |block|
289
296
  block[:media_types].each do |mt|
290
- unless rules_by_media.has_key?(mt)
297
+ unless rules_by_media.key?(mt)
291
298
  rules_by_media[mt] = []
292
299
  end
293
300
  rules_by_media[mt] << block[:rules]
@@ -299,15 +306,13 @@ module CssParser
299
306
 
300
307
  # Merge declarations with the same selector.
301
308
  def compact! # :nodoc:
302
- compacted = []
303
-
304
- compacted
309
+ []
305
310
  end
306
311
 
307
312
  def parse_block_into_rule_sets!(block, options = {}) # :nodoc:
308
313
  current_media_queries = [:all]
309
314
  if options[:media_types]
310
- current_media_queries = options[:media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
315
+ current_media_queries = options[:media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
311
316
  end
312
317
 
313
318
  in_declarations = 0
@@ -326,7 +331,7 @@ module CssParser
326
331
  rule_start = nil
327
332
  offset = nil
328
333
 
329
- block.scan(/\s+|[\\]{2,}|[\\]?[{}\s"]|.[^\s"{}\\]*/) do |token|
334
+ block.scan(/\s+|\\{2,}|\\?[{}\s"]|[()]|.[^\s"{}()\\]*/) do |token|
330
335
  # save the regex offset so that we know where in the file we are
331
336
  offset = Regexp.last_match.offset(0) if options[:capture_offsets]
332
337
 
@@ -349,7 +354,7 @@ module CssParser
349
354
  current_declarations << token
350
355
 
351
356
  if !in_string && token.include?('}')
352
- current_declarations.gsub!(/\}[\s]*$/, '')
357
+ current_declarations.gsub!(/\}\s*$/, '')
353
358
 
354
359
  in_declarations -= 1
355
360
  current_declarations.strip!
@@ -374,7 +379,7 @@ module CssParser
374
379
  current_media_queries = []
375
380
  elsif in_at_media_rule
376
381
  if token.include?('{')
377
- block_depth = block_depth + 1
382
+ block_depth += 1
378
383
  in_at_media_rule = false
379
384
  in_media_block = true
380
385
  current_media_queries << CssParser.sanitize_media_query(current_media_query)
@@ -388,43 +393,48 @@ module CssParser
388
393
  current_media_query = String.new
389
394
  else
390
395
  token.strip!
391
- current_media_query << token << ' '
396
+ # special-case the ( and ) tokens to remove inner-whitespace
397
+ # (eg we'd prefer '(width: 500px)' to '( width: 500px )' )
398
+ case token
399
+ when '('
400
+ current_media_query << token
401
+ when ')'
402
+ current_media_query.sub!(/ ?$/, token)
403
+ else
404
+ current_media_query << token << ' '
405
+ end
392
406
  end
393
407
  elsif in_charset or token =~ /@charset/i
394
408
  # iterate until we are out of the charset declaration
395
409
  in_charset = !token.include?(';')
396
- else
397
- if !in_string && token.include?('}')
398
- block_depth = block_depth - 1
399
-
400
- # reset the current media query scope
401
- if in_media_block
402
- current_media_queries = [:all]
403
- in_media_block = false
404
- end
405
- else
406
- if !in_string && token.include?('{')
407
- current_selectors.strip!
408
- in_declarations += 1
409
- else
410
- # if we are in a selector, add the token to the current selectors
411
- current_selectors << token
410
+ elsif !in_string && token.include?('}')
411
+ block_depth -= 1
412
412
 
413
- # mark this as the beginning of the selector unless we have already marked it
414
- rule_start = offset.first if options[:capture_offsets] && rule_start.nil? && token =~ /^[^\s]+$/
415
- end
413
+ # reset the current media query scope
414
+ if in_media_block
415
+ current_media_queries = [:all]
416
+ in_media_block = false
416
417
  end
418
+ elsif !in_string && token.include?('{')
419
+ current_selectors.strip!
420
+ in_declarations += 1
421
+ else
422
+ # if we are in a selector, add the token to the current selectors
423
+ current_selectors << token
424
+
425
+ # mark this as the beginning of the selector unless we have already marked it
426
+ rule_start = offset.first if options[:capture_offsets] && rule_start.nil? && token =~ /^[^\s]+$/
417
427
  end
418
428
  end
419
429
 
420
430
  # check for unclosed braces
421
- if in_declarations > 0
422
- if options[:capture_offsets]
423
- add_rule_with_offsets!(current_selectors, current_declarations, options[:filename], (rule_start..offset.last), current_media_queries)
424
- else
425
- add_rule!(current_selectors, current_declarations, current_media_queries)
426
- end
431
+ return unless in_declarations > 0
432
+
433
+ unless options[:capture_offsets]
434
+ return add_rule!(current_selectors, current_declarations, current_media_queries)
427
435
  end
436
+
437
+ add_rule_with_offsets!(current_selectors, current_declarations, options[:filename], (rule_start..offset.last), current_media_queries)
428
438
  end
429
439
 
430
440
  # Load a remote CSS file.
@@ -437,7 +447,7 @@ module CssParser
437
447
  def load_uri!(uri, options = {}, deprecated = nil)
438
448
  uri = Addressable::URI.parse(uri) unless uri.respond_to? :scheme
439
449
 
440
- opts = {:base_uri => nil, :media_types => :all}
450
+ opts = {base_uri: nil, media_types: :all}
441
451
 
442
452
  if options.is_a? Hash
443
453
  opts.merge!(options)
@@ -457,14 +467,13 @@ module CssParser
457
467
  opts[:filename] = uri.to_s if opts[:capture_offsets]
458
468
 
459
469
  src, = read_remote_file(uri) # skip charset
460
- if src
461
- add_block!(src, opts)
462
- end
470
+
471
+ add_block!(src, opts) if src
463
472
  end
464
473
 
465
474
  # Load a local CSS file.
466
475
  def load_file!(file_name, options = {}, deprecated = nil)
467
- opts = {:base_dir => nil, :media_types => :all}
476
+ opts = {base_dir: nil, media_types: :all}
468
477
 
469
478
  if options.is_a? Hash
470
479
  opts.merge!(options)
@@ -487,7 +496,7 @@ module CssParser
487
496
 
488
497
  # Load a local CSS string.
489
498
  def load_string!(src, options = {}, deprecated = nil)
490
- opts = {:base_dir => nil, :media_types => :all}
499
+ opts = {base_dir: nil, media_types: :all}
491
500
 
492
501
  if options.is_a? Hash
493
502
  opts.merge!(options)
@@ -499,9 +508,8 @@ module CssParser
499
508
  add_block!(src, opts)
500
509
  end
501
510
 
502
-
503
-
504
511
  protected
512
+
505
513
  # Check that a path hasn't been loaded already
506
514
  #
507
515
  # Raises a CircularReferenceError exception if io_exceptions are on,
@@ -510,10 +518,11 @@ module CssParser
510
518
  path = path.to_s
511
519
  if @loaded_uris.include?(path)
512
520
  raise CircularReferenceError, "can't load #{path} more than once" if @options[:io_exceptions]
513
- return false
521
+
522
+ false
514
523
  else
515
524
  @loaded_uris << path
516
- return true
525
+ true
517
526
  end
518
527
  end
519
528
 
@@ -541,7 +550,7 @@ module CssParser
541
550
  utf8_block = ignore_pattern(utf8_block, STRIP_HTML_COMMENTS_RX, options)
542
551
 
543
552
  # Strip lines containing just whitespace
544
- utf8_block.gsub!(/^\s+$/, "") unless options[:capture_offsets]
553
+ utf8_block.gsub!(/^\s+$/, '') unless options[:capture_offsets]
545
554
 
546
555
  utf8_block
547
556
  end
@@ -577,11 +586,8 @@ module CssParser
577
586
  if uri.scheme == 'file'
578
587
  # local file
579
588
  path = uri.path
580
- path.gsub!(/^\//, '') if Gem.win_platform?
581
- fh = open(path, 'rb')
582
- src = fh.read
583
- charset = fh.respond_to?(:charset) ? fh.charset : 'utf-8'
584
- fh.close
589
+ path.gsub!(%r{^/}, '') if Gem.win_platform?
590
+ src = File.read(path, mode: 'rb')
585
591
  else
586
592
  # remote file
587
593
  if uri.scheme == 'https'
@@ -593,27 +599,28 @@ module CssParser
593
599
  http = Net::HTTP.new(uri.host, uri.port)
594
600
  end
595
601
 
596
- res = http.get(uri.request_uri, {'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip'})
602
+ res = http.get(uri.request_uri, {'User-Agent' => @options[:user_agent], 'Accept-Encoding' => 'gzip'})
597
603
  src = res.body
598
604
  charset = res.respond_to?(:charset) ? res.encoding : 'utf-8'
599
605
 
600
606
  if res.code.to_i >= 400
601
607
  @redirect_count = nil
602
- raise RemoteFileError.new(uri.to_s) if @options[:io_exceptions]
608
+ raise RemoteFileError, uri.to_s if @options[:io_exceptions]
609
+
603
610
  return '', nil
604
611
  elsif res.code.to_i >= 300 and res.code.to_i < 400
605
- if res['Location'] != nil
612
+ unless res['Location'].nil?
606
613
  return read_remote_file Addressable::URI.parse(Addressable::URI.escape(res['Location']))
607
614
  end
608
615
  end
609
616
 
610
617
  case res['content-encoding']
611
- when 'gzip'
612
- io = Zlib::GzipReader.new(StringIO.new(res.body))
613
- src = io.read
614
- when 'deflate'
615
- io = Zlib::Inflate.new
616
- src = io.inflate(res.body)
618
+ when 'gzip'
619
+ io = Zlib::GzipReader.new(StringIO.new(res.body))
620
+ src = io.read
621
+ when 'deflate'
622
+ io = Zlib::Inflate.new
623
+ src = io.inflate(res.body)
617
624
  end
618
625
  end
619
626
 
@@ -627,15 +634,17 @@ module CssParser
627
634
  end
628
635
  rescue
629
636
  @redirect_count = nil
630
- raise RemoteFileError.new(uri.to_s)if @options[:io_exceptions]
637
+ raise RemoteFileError, uri.to_s if @options[:io_exceptions]
638
+
631
639
  return nil, nil
632
640
  end
633
641
 
634
642
  @redirect_count = nil
635
- return src, charset
643
+ [src, charset]
636
644
  end
637
645
 
638
646
  private
647
+
639
648
  # Save a folded declaration block to the internal cache.
640
649
  def save_folded_declaration(block_hash, folded_declaration) # :nodoc:
641
650
  @folded_declaration_cache[block_hash] = folded_declaration
@@ -643,7 +652,7 @@ module CssParser
643
652
 
644
653
  # Retrieve a folded declaration block from the internal cache.
645
654
  def get_folded_declaration(block_hash) # :nodoc:
646
- return @folded_declaration_cache[block_hash] ||= nil
655
+ @folded_declaration_cache[block_hash] ||= nil
647
656
  end
648
657
 
649
658
  def reset! # :nodoc:
@@ -657,14 +666,15 @@ module CssParser
657
666
  # passed hash
658
667
  def css_node_to_h(hash, key, val)
659
668
  hash[key.strip] = '' and return hash if val.nil?
669
+
660
670
  lines = val.split(';')
661
671
  nodes = {}
662
672
  lines.each do |line|
663
673
  parts = line.split(':', 2)
664
- if (parts[1] =~ /:/)
674
+ if parts[1] =~ /:/
665
675
  nodes[parts[0]] = css_node_to_h(hash, parts[0], parts[1])
666
676
  else
667
- nodes[parts[0].to_s.strip] =parts[1].to_s.strip
677
+ nodes[parts[0].to_s.strip] = parts[1].to_s.strip
668
678
  end
669
679
  end
670
680
  hash[key.strip] = nodes