Skip to content
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

Scss scanner #162

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions lib/coderay/helpers/file_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def type_from_shebang filename
'ru' => :ruby, # config.ru
'rxml' => :ruby,
'sass' => :sass,
'scss' => :sass,
'sql' => :sql,
'taskpaper' => :taskpaper,
'template' => :json, # AWS CloudFormation template
Expand Down
222 changes: 222 additions & 0 deletions lib/coderay/scanners/scss.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
module CodeRay
module Scanners

# A scanner for Sass, in scss syntax.
class SCSS < CSS

register_for :scss
file_extension 'scss'

protected

def setup
@state = :initial
end

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

encoder.begin_group :string if states.last == :sqstring || states.last == :dqstring

until eos?

if bol? && (match = scan(/(?>( +)?(\/[\*\/])(.+)?)(?=\n)/))
encoder.text_token self[1], :space if self[1]
encoder.begin_group :comment
encoder.text_token self[2], :delimiter
encoder.text_token self[3], :content if self[3]
if match = scan(/(?:\n+#{self[1]} .*)+/)
encoder.text_token match, :content
end
encoder.end_group :comment
elsif match = scan(/\n|[^\n\S]+\n?/)
encoder.text_token match, :space
if match.index(/\n/)
value_expected = false
states.pop if states.last == :include
end

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(/\S*:(?![a-z])/) ? :key : :tag)
next
elsif !value_expected && (match = scan(/\*/))
encoder.text_token match, :tag
next
# TODO: update RE:Name to add "%" as valid start of class name
elsif match = scan(/(\.|\%)[^\d][-_a-zA-Z\d#\{\}\$]+/)
encoder.text_token match, :class
next
elsif match = scan(RE::Id)
encoder.text_token match, :id
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(/@import\b/)
encoder.text_token match, :directive
states << :include
next
elsif match = scan(/@media\b/)
encoder.text_token match, :directive
# states.push :media_before_name
next
end

when :sqstring, :dqstring
if match = scan(states.last == :sqstring ? /(?:[^\n\'\#]+|\\\n|#{RE::Escape}|#(?!\{))+/o : /(?:[^\n\"\#]+|\\\n|#{RE::Escape}|#(?!\{))+/o)
encoder.text_token match, :content
elsif match = scan(/['"]/)
encoder.text_token match, :delimiter
encoder.end_group :string
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 states.last
encoder.text_token match, :error unless match.empty?
states.pop
else
raise_inspect "else case #{states.last} reached; %p not handled." % peek(1), encoder
end

when :include
if match = scan(/[^\s'",]+/)
encoder.text_token match, :include
next
end

else
#:nocov:
raise_inspect 'Unknown state: %p' % [states.last], 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

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
encoder.text_token match, :delimiter
if states.include? :sass_inline
# no nesting, just scan the string until delimiter
content = scan_until(/(?=#{match}|\}|\z)/)
encoder.text_token content, :content unless content.empty?
encoder.text_token match, :delimiter if scan(/#{match}/)
encoder.end_group :string
else
states.push match == "'" ? :sqstring : :dqstring
end

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 if start.size < match.size
end
encoder.end_group :function

elsif match = scan(/[a-z][-a-z_]*(?=\()/o)
encoder.text_token match, :predefined

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|default)/)
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}/o)
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

states.pop if states.last == :include

if options[:keep_state]
@state = states.dup
end

while state = states.pop
if state == :sass_inline
encoder.end_group :inline
elsif state == :sqstring || state == :dqstring
encoder.end_group :string
end
end

encoder
end

end

end
end