Skip to content

Sass scanner #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 9, 2013
1 change: 1 addition & 0 deletions Changes.textile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ p=. _This files lists all changes in the CodeRay library since the 0.9.8 release

h2. Changes in 1.1

* New scanner: Sass [#93]
* Diff scanner: Highlight inline changes in multi-line changes [#99]
* Remove double-click toggle handler from HTML table output
* Fixes to CSS scanner (floats, pseudoclasses)
Expand Down
1 change: 1 addition & 0 deletions lib/coderay/helpers/file_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def shebang filename
'rpdf' => :ruby,
'ru' => :ruby,
'rxml' => :ruby,
'sass' => :sass,
# 'sch' => :scheme,
'sql' => :sql,
# 'ss' => :scheme,
Expand Down
31 changes: 14 additions & 17 deletions lib/coderay/scanners/css.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,41 @@ class CSS < Scanner

module RE # :nodoc:
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]|#{Escape}/
NMStart = /[_a-zA-Z]|#{Escape}/
NL = /\r\n|\r|\n|\f/
String1 = /"(?:[^\n\r\f\\"]|\\#{NL}|#{Escape})*"?/ # TODO: buggy regexp
String2 = /'(?:[^\n\r\f\\']|\\#{NL}|#{Escape})*'?/ # TODO: buggy regexp
Unicode = /\\#{Hex}{1,6}\b/ # differs from standard because it allows uppercase hex too
Escape = /#{Unicode}|\\[^\n0-9a-fA-F]/
NMChar = /[-_a-zA-Z0-9]/
NMStart = /[_a-zA-Z]/
String1 = /"(?:[^\n\\"]+|\\\n|#{Escape})*"?/ # TODO: buggy regexp
String2 = /'(?:[^\n\\']+|\\\n|#{Escape})*'?/ # TODO: buggy regexp
String = /#{String1}|#{String2}/

HexColor = /#(?:#{Hex}{6}|#{Hex}{3})/
Color = /#{HexColor}/

Num = /-?(?:[0-9]+(?!\.\d)|[0-9]*\.[0-9]+)/
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 + %w[s]))
Unit = Regexp.union(*(reldimensions + absdimensions + %w[s dpi dppx deg]))

Dimension = /#{Num}#{Unit}/

Comment = %r! /\* (?: .*? \*/ | .* ) !mx
Function = /(?:url|alpha|attr|counters?)\((?:[^)\n\r\f]|\\\))*\)?/
Function = /(?:url|alpha|attr|counters?)\((?:[^)\n]|\\\))*\)?/

Id = /##{Name}/
Id = /(?!#{HexColor}\b(?!-))##{Name}/
Class = /\.#{Name}/
PseudoClass = /::?#{Name}/
PseudoClass = /::?#{Ident}/
AttributeSelector = /\[[^\]]*\]?/
end

protected

def setup
@state = :initial
@value_expected = nil
@value_expected = false
end

def scan_tokens encoder, options
Expand Down Expand Up @@ -158,7 +155,7 @@ def scan_tokens encoder, options
elsif match = scan(/(?: #{RE::Dimension} | #{RE::Percentage} | #{RE::Num} )/ox)
encoder.text_token match, :float

elsif match = scan(/#{RE::Color}/o)
elsif match = scan(/#{RE::HexColor}/o)
encoder.text_token match, :color

elsif match = scan(/! *important/)
Expand All @@ -170,7 +167,7 @@ def scan_tokens encoder, options
elsif match = scan(RE::AtKeyword)
encoder.text_token match, :directive

elsif match = scan(/ [+>:;,.=()\/] /x)
elsif match = scan(/ [+>~:;,.=()\/] /x)
if match == ':'
value_expected = true
elsif match == ';'
Expand Down
207 changes: 207 additions & 0 deletions lib/coderay/scanners/sass.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
module CodeRay
module Scanners

# A scanner for Sass.
class Sass < CSS

register_for :sass
file_extension 'sass'

SASS_FUNCTION = /(?:inline-image|linear-gradient|color-stops|mix|lighten|darken|rotate|image-url|image-width|image-height|sprite-url|sprite-path|sprite-file|sprite-map|sprite-position|sprite|unquote|join|round|ceil|floor|nth)/

STRING_CONTENT_PATTERN = {
"'" => /(?:[^\n\'\#]+|\\\n|#{RE::Escape}|#(?!\{))+/,
'"' => /(?:[^\n\"\#]+|\\\n|#{RE::Escape}|#(?!\{))+/,
}

protected

def setup
@state = :initial
end

def scan_tokens encoder, options
states = Array(options[:state] || @state)
string_delimiter = nil

until eos?

if match = scan(/\s+/)
encoder.text_token match, :space
value_expected = false if match.index(/\n/)

elsif states.last == :sass_inline && (match = scan(/\}/))
encoder.text_token match, :inline_delimiter
encoder.end_group :inline
states.pop

elsif case states.last
when :initial, :media, :sass_inline
if match = scan(/(?>#{RE::Ident})(?!\()/ox)
encoder.text_token match, value_expected ? :value : (check(/.*:/) ? :key : :type)
next
elsif !value_expected && (match = scan(/\*/))
encoder.text_token match, :type
next
elsif match = scan(RE::Class)
encoder.text_token match, :class
next
elsif match = scan(RE::Id)
encoder.text_token match, :constant
next
elsif match = scan(RE::PseudoClass)
encoder.text_token match, :pseudo_class
next
elsif match = scan(RE::AttributeSelector)
# TODO: Improve highlighting inside of attribute selectors.
encoder.text_token match[0,1], :operator
encoder.text_token match[1..-2], :attribute_name if match.size > 2
encoder.text_token match[-1,1], :operator if match[-1] == ?]
next
elsif match = scan(/(\=|@mixin +)#{RE::Ident}/o)
encoder.text_token match, :function
next
elsif match = scan(/@media/)
encoder.text_token match, :directive
# states.push :media_before_name
next
end

when :block
if match = scan(/(?>#{RE::Ident})(?!\()/ox)
if value_expected
encoder.text_token match, :value
else
encoder.text_token match, :key
end
next
end

when :string
if match = scan(STRING_CONTENT_PATTERN[string_delimiter])
encoder.text_token match, :content
elsif match = scan(/['"]/)
encoder.text_token match, :delimiter
encoder.end_group :string
string_delimiter = nil
states.pop
elsif match = scan(/#\{/)
encoder.begin_group :inline
encoder.text_token match, :inline_delimiter
states.push :sass_inline
elsif match = scan(/ \\ | $ /x)
encoder.end_group state
encoder.text_token match, :error unless match.empty?
states.pop
else
raise_inspect "else case #{string_delimiter} reached; %p not handled." % peek(1), encoder
end

else
#:nocov:
raise_inspect 'Unknown state', encoder
#:nocov:

end

elsif match = scan(/\$#{RE::Ident}/o)
encoder.text_token match, :variable
next

elsif match = scan(/&/)
encoder.text_token match, :local_variable

elsif match = scan(/\+#{RE::Ident}/o)
encoder.text_token match, :include
value_expected = true

elsif match = scan(/\/\*(?:.*?\*\/|.*)|\/\/.*/)
encoder.text_token match, :comment

elsif match = scan(/#\{/)
encoder.begin_group :inline
encoder.text_token match, :inline_delimiter
states.push :sass_inline

elsif match = scan(/\{/)
value_expected = false
encoder.text_token match, :operator
states.push :block

elsif match = scan(/\}/)
value_expected = false
encoder.text_token match, :operator
if states.last == :block || states.last == :media
states.pop
end

elsif match = scan(/['"]/)
encoder.begin_group :string
string_delimiter = match
encoder.text_token match, :delimiter
if states.include? :sass_inline
content = scan_until(/(?=#{string_delimiter}|\}|\z)/)
encoder.text_token content, :content unless content.empty?
encoder.text_token string_delimiter, :delimiter if scan(/#{string_delimiter}/)
encoder.end_group :string
else
states.push :string
end

elsif match = scan(/#{SASS_FUNCTION}/o)
encoder.text_token match, :predefined

elsif match = scan(/#{RE::Function}/o)
encoder.begin_group :function
start = match[/^[-\w]+\(/]
encoder.text_token start, :delimiter
if match[-1] == ?)
encoder.text_token match[start.size..-2], :content
encoder.text_token ')', :delimiter
else
encoder.text_token match[start.size..-1], :content
end
encoder.end_group :function

elsif match = scan(/(?: #{RE::Dimension} | #{RE::Percentage} | #{RE::Num} )/ox)
encoder.text_token match, :float

elsif match = scan(/#{RE::HexColor}/o)
encoder.text_token match, :color

elsif match = scan(/! *(?:important|optional)/)
encoder.text_token match, :important

elsif match = scan(/(?:rgb|hsl)a?\([^()\n]*\)?/)
encoder.text_token match, :color

elsif match = scan(/@else if\b|#{RE::AtKeyword}/)
encoder.text_token match, :directive
value_expected = true

elsif match = scan(/ == | != | [-+*\/>~:;,.=()] /x)
if match == ':'
value_expected = true
elsif match == ';'
value_expected = false
end
encoder.text_token match, :operator

else
encoder.text_token getch, :error

end

end

if options[:keep_state]
@state = states
end

encoder
end

end

end
end
2 changes: 2 additions & 0 deletions lib/coderay/styles/alpha.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class Alpha < Style
.exception { color:#C00; font-weight:bold }
.float { color:#60E }
.function { color:#06B; font-weight:bold }
.function .delimiter { color:#024; font-weight:bold }
.global-variable { color:#d70 }
.hex { color:#02b }
.imaginary { color:#f00 }
Expand All @@ -86,6 +87,7 @@ class Alpha < Style
.inline-delimiter { font-weight: bold; color: #666 }
.instance-variable { color:#33B }
.integer { color:#00D }
.important { color:#D00 }
.key .char { color: #60f }
.key .delimiter { color: #404 }
.key { color: #606 }
Expand Down