diff --git a/.travis.yml b/.travis.yml index 18a30f6..cff6ae5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,19 @@ sudo: false +dist: trusty language: ruby rvm: - - 2.5.1 -before_install: gem install bundler -v 1.16.2 + - "2.0" + - "2.1" + - "2.2" + - "2.3" + - "2.4" + - "2.5" + +cache: + bundler: true +bundler_args: --without development +before_install: + - gem install bundler + - gem update --system +script: + - bundle exec rake diff --git a/README.md b/README.md index 10328fd..845f7ef 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Rouge::Lexers::Crystal -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rouge/lexers/crystal`. To experiment with that code, run `bin/console` for an interactive prompt. +[Crystal](https://crystal-lang.org) lexer for [rouge](http://rouge.jneen.net/). -TODO: Delete this and the text above, and describe your gem +This gem exists because rouge lacks support for the Crystal language. These PRs are pending: [441](/~https://github.com/jneen/rouge/pull/441) and [863](/~https://github.com/jneen/rouge/pull/863). ## Installation @@ -22,7 +22,8 @@ Or install it yourself as: ## Usage -TODO: Write usage instructions here +See rouge's [README](/~https://github.com/jneen/rouge/blob/master/README.md) for the full description. + ## Development diff --git a/Rakefile b/Rakefile index d433a1e..caed168 100644 --- a/Rakefile +++ b/Rakefile @@ -2,9 +2,9 @@ require "bundler/gem_tasks" require "rake/testtask" Rake::TestTask.new(:test) do |t| - t.libs << "test" + t.libs << "spec" t.libs << "lib" - t.test_files = FileList["test/**/*_test.rb"] + t.test_files = FileList["spec/**/*_spec.rb"] end task :default => :test diff --git a/lib/rouge-lexers-crystal.rb b/lib/rouge-lexers-crystal.rb new file mode 100644 index 0000000..ff0c006 --- /dev/null +++ b/lib/rouge-lexers-crystal.rb @@ -0,0 +1 @@ +require "rouge/lexers/crystal" diff --git a/lib/rouge/lexers/crystal.rb b/lib/rouge/lexers/crystal.rb index 3be3071..ec7980a 100644 --- a/lib/rouge/lexers/crystal.rb +++ b/lib/rouge/lexers/crystal.rb @@ -1,9 +1,439 @@ -require "rouge/lexers/crystal/version" +# -*- coding: utf-8 -*- # + +require 'rouge' module Rouge module Lexers - module Crystal - # Your code goes here... + class Crystal < RegexLexer + title "Crystal" + desc "Crystal The Programming Language (crystal-lang.org)" + tag 'crystal' + aliases 'cr' + filenames '*.cr' + + mimetypes 'text/x-crystal', 'application/x-crystal' + + # This plugin supports all versions of rouge + # TODO: Remove move + if Rouge.version >= '3.0.0' + def self.detect?(text) + return true if text.shebang? 'crystal' + end + else + def self.analyze_text(text) + return 1 if text.shebang? 'crystal' + end + end + + state :symbols do + # symbols + rule %r( + : # initial : + @{0,2} # optional ivar, for :@foo and :@@foo + [a-z_]\w*[!?]? # the symbol + )xi, Str::Symbol + + # special symbols + rule %r(:(?:\*\*|[-+]@|[/\%&\|^`~]|\[\]=?|<<|>>|<=?>|<=?|===?)), + Str::Symbol + + rule /:'(\\\\|\\'|[^'])*'/, Str::Symbol + rule /:"/, Str::Symbol, :simple_sym + end + + state :sigil_strings do + # %-sigiled strings + # %(abc), %[abc], %, %.abc., %r.abc., etc + delimiter_map = { '{' => '}', '[' => ']', '(' => ')', '<' => '>' } + rule /%([rqswQWxiI])?([^\w\s])/ do |m| + open = Regexp.escape(m[2]) + close = Regexp.escape(delimiter_map[m[2]] || m[2]) + interp = /[rQWxI]/ === m[1] + toktype = Str::Other + + puts " open: #{open.inspect}" if @debug + puts " close: #{close.inspect}" if @debug + + # regexes + if m[1] == 'r' + toktype = Str::Regex + push :regex_flags + end + + token toktype + + push do + rule /\\[##{open}#{close}\\]/, Str::Escape + # nesting rules only with asymmetric delimiters + if open != close + rule /#{open}/ do + token toktype + push + end + end + rule /#{close}/, toktype, :pop! + + if interp + mixin :string_intp_escaped + rule /#/, toktype + else + rule /[\\#]/, toktype + end + + rule /[^##{open}#{close}\\]+/m, toktype + end + end + end + + state :strings do + mixin :symbols + rule /\b[a-z_]\w*?[?!]?:\s+/, Str::Symbol, :expr_start + rule /'(\\\\|\\'|[^'])*'/, Str::Single + rule /"/, Str::Double, :simple_string + rule /(?_*\$?:"]), Name::Variable::Global + rule /\$-[0adFiIlpvw]/, Name::Variable::Global + rule /::/, Operator + + mixin :strings + + rule /(?:#{keywords.join('|')})\b/, Keyword, :expr_start + rule /(?:#{keywords_pseudo.join('|')})\b/, Keyword::Pseudo, :expr_start + + rule %r( + (module) + (\s+) + ([a-zA-Z_][a-zA-Z0-9_]*(::[a-zA-Z_][a-zA-Z0-9_]*)*) + )x do + groups Keyword, Text, Name::Namespace + end + + rule /(def\b)(\s*)/ do + groups Keyword, Text + push :funcname + end + + rule /(class\b)(\s*)/ do + groups Keyword, Text + push :classname + end + + rule /(?:#{builtins_q.join('|')})[?]/, Name::Builtin, :expr_start + rule /(?:#{builtins_b.join('|')})!/, Name::Builtin, :expr_start + rule /(?=])/ do + groups Punctuation, Text, Name::Function + push :method_call + end + + rule /[a-zA-Z_]\w*[?!]/, Name, :expr_start + rule /[a-zA-Z_]\w*/, Name, :method_call + rule /\*\*|<>?|>=|<=|<=>|=~|={3}|!~|&&?|\|\||\./, + Operator, :expr_start + rule /[-+\/*%=<>&!^|~]=?/, Operator, :expr_start + rule(/[?]/) { token Punctuation; push :ternary; push :expr_start } + rule %r<[\[({,:\\;/]>, Punctuation, :expr_start + rule %r<[\])}]>, Punctuation + end + + state :has_heredocs do + rule /(?>? | <=>? | >= | ===? + ) + )x do |m| + puts "matches: #{[m[0], m[1], m[2], m[3]].inspect}" if @debug + groups Name::Class, Operator, Name::Function + pop! + end + + rule(//) { pop! } + end + + state :classname do + rule /\s+/, Text + rule /\(/ do + token Punctuation + push :defexpr + push :expr_start + end + + # class << expr + rule /<=0?n[x]:"" + rule %r( + [?](\\[MC]-)* # modifiers + (\\([\\abefnrstv\#"']|x[a-fA-F0-9]{1,2}|[0-7]{1,3})|\S) + (?!\w) + )x, Str::Char, :pop! + + # special case for using a single space. Ruby demands that + # these be in a single line, otherwise it would make no sense. + rule /(\s*)(%[rqswQWxiI]? \S* )/ do + groups Text, Str::Other + pop! + end + + mixin :sigil_strings + + rule(//) { pop! } + end + + state :slash_regex do + mixin :string_intp + rule %r(\\\\), Str::Regex + rule %r(\\/), Str::Regex + rule %r([\\#]), Str::Regex + rule %r([^\\/#]+)m, Str::Regex + rule %r(/) do + token Str::Regex + goto :regex_flags + end + end + + state :end_part do + # eat up the rest of the stream as Comment::Preproc + rule /.+/m, Comment::Preproc, :pop! + end end end end diff --git a/lib/rouge/lexers/crystal/version.rb b/lib/rouge/lexers/crystal/version.rb deleted file mode 100644 index fa4f595..0000000 --- a/lib/rouge/lexers/crystal/version.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Rouge - module Lexers - module Crystal - VERSION = "0.1.0" - end - end -end diff --git a/rouge-lexers-crystal.gemspec b/rouge-lexers-crystal.gemspec index 3bfbfcc..bd49abf 100644 --- a/rouge-lexers-crystal.gemspec +++ b/rouge-lexers-crystal.gemspec @@ -1,28 +1,19 @@ lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require "rouge/lexers/crystal/version" +require "rouge/lexers/crystal" Gem::Specification.new do |spec| spec.name = "rouge-lexers-crystal" - spec.version = Rouge::Lexers::Crystal::VERSION + spec.version = "1.0.0.rc1" spec.authors = ["Peter Leitzen"] spec.email = ["peter@leitzen.de"] - spec.summary = %q{TODO: Write a short summary, because RubyGems requires one.} - spec.description = %q{TODO: Write a longer description or delete this line.} - spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.summary = %q{Crystal lexer for rouge} + spec.description = %q{/~https://github.com/jneen/rouge/pull/441} + spec.homepage = "/~https://github.com/splattae/rouge-lexers-crystal" spec.license = "MIT" - # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' - # to allow pushing to a single host or delete this section to allow pushing to any host. - if spec.respond_to?(:metadata) - spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" - else - raise "RubyGems 2.0 or newer is required to protect against " \ - "public gem pushes." - end - # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do @@ -35,4 +26,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.16" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "minitest", "~> 5.0" + spec.add_development_dependency "minitest-power_assert" + + spec.add_runtime_dependency "rouge" end diff --git a/spec/lexers/crystal_spec.rb b/spec/lexers/crystal_spec.rb new file mode 100644 index 0000000..1db177e --- /dev/null +++ b/spec/lexers/crystal_spec.rb @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- # + +require 'spec_helper' + +require 'rouge/lexers/crystal' + +describe Rouge::Lexers::Crystal do + let(:subject) { Rouge::Lexers::Crystal.new } + + describe 'guessing' do + include Support::Guessing + + it 'guesses by filename' do + assert_guess :filename => 'foo.cr' + end + + it 'guesses by mimetype' do + assert_guess :mimetype => 'text/x-crystal' + assert_guess :mimetype => 'application/x-crystal' + end + + it 'guesses by source' do + assert_guess :source => '#!/usr/local/bin/crystal' + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..4838c3c --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,7 @@ +require 'bundler/setup' +require 'minitest/power_assert' +require 'minitest/autorun' + +require 'rouge' + +require_relative 'support/guessing' diff --git a/spec/support/guessing.rb b/spec/support/guessing.rb new file mode 100644 index 0000000..3f706f9 --- /dev/null +++ b/spec/support/guessing.rb @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- # + +module Support + module Guessing + def assert_guess(type=nil, info={}) + if type.is_a? Hash + info = type + type = nil + end + + type ||= subject.class + + assert { Rouge::Lexer.guess(info) == type } + Rouge::Lexer.all.reverse! + + assert { Rouge::Lexer.guess(info) == type } + Rouge::Lexer.all.reverse! + end + + def deny_guess(type=nil, info={}) + if type.is_a? Hash + info = type + type = nil + end + + type ||= subject.class + + refute { Rouge::Lexer.guess(info) == type } + Rouge::Lexer.all.reverse! + + refute { Rouge::Lexer.guess(info) == type } + Rouge::Lexer.all.reverse! + end + end +end diff --git a/test/rouge/lexers/crystal_test.rb b/test/rouge/lexers/crystal_test.rb deleted file mode 100644 index 996f3f9..0000000 --- a/test/rouge/lexers/crystal_test.rb +++ /dev/null @@ -1,11 +0,0 @@ -require "test_helper" - -class Rouge::Lexers::CrystalTest < Minitest::Test - def test_that_it_has_a_version_number - refute_nil ::Rouge::Lexers::Crystal::VERSION - end - - def test_it_does_something_useful - assert false - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index e785e7a..0000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) -require "rouge/lexers/crystal" - -require "minitest/autorun"