module CodeRay module Scanners class CSS < Scanner register_for :css module RE NonASCII = /[\x80-\xFF]/ Hex = /[0-9a-fA-F]/ Unicode = /\\#{Hex}{1,6}(?:\r\n|\s)?/ # differs from standard because it allows uppercase hex too Escape = /#{Unicode}|\\[^\r\n\f0-9a-fA-F]/ NMChar = /[-_a-zA-Z0-9]|#{NonASCII}|#{Escape}/ NMStart = /[_a-zA-Z]|#{NonASCII}|#{Escape}/ NL = /\r\n|\r|\n|\f/ String1 = /"(?:[^\n\r\f\\"]|\\#{NL}|#{Escape})*"?/ # FIXME: buggy regexp String2 = /'(?:[^\n\r\f\\']|\\#{NL}|#{Escape})*'?/ # FIXME: buggy regexp String = /#{String1}|#{String2}/ HexColor = /#(?:#{Hex}{6}|#{Hex}{3})/ Color = /#{HexColor}/ Num = /-?(?:[0-9]+|[0-9]*\.[0-9]+)/ Name = /#{NMChar}+/ Ident = /-?#{NMStart}#{NMChar}*/ AtKeyword = /@#{Ident}/ Percentage = /#{Num}%/ reldimensions = %w[em ex px] absdimensions = %w[in cm mm pt pc] Unit = Regexp.union(*(reldimensions + absdimensions)) Dimension = /#{Num}#{Unit}/ Comment = %r! /\* (?: .*? \*/ | .* ) !mx Function = /(?:url|alpha)\((?:[^)\n\r\f]|\\\))*\)?/ Id = /##{Name}/ Class = /\.#{Name}/ PseudoClass = /:#{Name}/ end def scan_tokens tokens, options value_expected = nil states = [:initial] until eos? kind = nil match = nil if scan(/\s+/) kind = :space elsif case states.last when :initial if scan(/#{RE::Ident}|\*/ox) kind = :keyword elsif scan RE::Class kind = :class elsif scan RE::Id kind = :constant elsif scan RE::PseudoClass kind = :pseudo_class elsif scan RE::Name kind = :identifier end when :block if scan(/(?>#{RE::Ident})(?!\()/ox) if value_expected kind = :value else kind = :key end end when :comment if scan(/(?:[^*\s]|\*(?!\/))+/) kind = :comment elsif scan(/\*\//) kind = :comment states.pop elsif scan(/\s+/) kind = :space end else raise_inspect 'Unknown state', tokens end elsif scan(/\/\*/) kind = :comment states.push :comment elsif scan(/\{/) value_expected = false kind = :operator states.push :block elsif scan(/\}/) value_expected = false if states.last == :block kind = :operator states.pop else kind = :error end elsif match = scan(/#{RE::String}/o) tokens << [:open, :string] tokens << [match[0, 1], :delimiter] tokens << [match[1..-2], :content] if match.size > 2 tokens << [match[-1, 1], :delimiter] if match.size >= 2 tokens << [:close, :string] next elsif match = scan(/#{RE::Function}/o) tokens << [:open, :string] start = match[/^\w+\(/] tokens << [start, :delimiter] if match[-1] == ?) tokens << [match[start.size..-2], :content] tokens << [')', :delimiter] else tokens << [match[start.size..-1], :content] end tokens << [:close, :string] next elsif scan(/(?: #{RE::Dimension} | #{RE::Percentage} | #{RE::Num} )/ox) kind = :float elsif scan(/#{RE::Color}/o) kind = :color elsif scan(/! *important/) kind = :important elsif scan(/rgb\([^()\n]*\)?/) kind = :color elsif scan(/#{RE::AtKeyword}/o) kind = :directive elsif match = scan(/ [+>:;,.=()\/] /x) if match == ':' value_expected = true elsif match == ';' value_expected = false end kind = :operator else getch kind = :error end match ||= matched if $DEBUG and not kind raise_inspect 'Error token %p in line %d' % [[match, kind], line], tokens end raise_inspect 'Empty token', tokens unless match tokens << [match, kind] # tokens << [states.inspect, :error] end tokens end end end end