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