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