diff --git a/lib/rouge/demos/livescript b/lib/rouge/demos/livescript new file mode 100644 index 0000000000..9daf4db300 --- /dev/null +++ b/lib/rouge/demos/livescript @@ -0,0 +1,15 @@ +mitch = + age: 21 + height: 180cm + pets: [\dog, \goldfish] + +phile = {} +phile{height, pets} = mitch +phile.height #=> 180 +phile.pets #=> ['dog', 'goldfish'] + +a = [2 7 1 8] + ..push 3 + ..shift! + ..sort! +a #=> [1,3,7,8] diff --git a/lib/rouge/lexers/livescript.rb b/lib/rouge/lexers/livescript.rb new file mode 100644 index 0000000000..2428d0c5b9 --- /dev/null +++ b/lib/rouge/lexers/livescript.rb @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- # +# frozen_string_literal: true + +module Rouge + module Lexers + class Livescript < RegexLexer + tag 'livescript' + aliases 'ls' + filenames '*.ls' + mimetypes 'text/livescript' + + title 'LiveScript' + desc 'LiveScript, a language which compiles to JavaScript (livescript.net)' + + def self.detect?(text) + return text.shebang? 'lsc' + end + + def self.declarations + @declarations ||= Set.new %w(const let var function class extends implements) + end + + def self.keywords + @keywords ||= Set.new %w( + loop until for in of while break return continue switch case + fallthrough default otherwise when then if unless else throw try + catch finally new delete typeof instanceof super by from to til + with require do debugger import export yield + ) + end + + def self.constants + @constants ||= Javascript.constants + %w(yes no on off void) + end + + def self.builtins + @builtins ||= Javascript.builtins + %w(this it that arguments) + end + + def self.loop_control_keywords + @loop_control_keywords ||= Set.new %w(break continue) + end + + id = /[$a-z_]((-(?=[a-z]))?[a-z0-9_])*/i + int_number = /\d[\d_]*/ + int = /#{int_number}(e[+-]?#{int_number})?[$\w]*/ # the last class matches units + + state :root do + rule(%r(^(?=\s|/))) { push :slash_starts_regex } + mixin :comments + mixin :whitespace + + # list of words + rule %r/(<\[)(.*?)(\]>)/m do + groups Punctuation, Str, Punctuation + end + + # function declarations + rule %r/!\s*function\b/, Keyword::Declaration + rule %r/!?[-~]>|<[-~]!?/, Keyword::Declaration + + # switch arrow + rule %r/(=>)/, Keyword + + # prototype attributes + rule %r/(::)(#{id})/ do + groups Punctuation, Name::Attribute + push :id + end + rule %r/(::)(#{int})/ do + groups Punctuation, Num::Integer + push :id + end + + # instance attributes + rule %r/(@)(#{id})/ do + groups Name::Variable::Instance, Name::Attribute + push :id + end + rule %r/([.])(#{id})/ do + groups Punctuation, Name::Attribute + push :id + end + rule %r/([.])(\d+)/ do + groups Punctuation, Num::Integer + push :id + end + rule %r/#{id}(?=\s*:[^:=])/, Name::Attribute + + # operators + rule %r( + [+][+]|--|&&|\b(and|x?or|is(nt)?|not)\b(?!-[a-zA-Z]|_)|[|][|]| + [.]([|&^]|<<|>>>?)[.]|\\(?=\n)|[.:]=|<<<| + (<<|>>|==?|!=?|[-<>+*%^/~?])=? + )x, Operator, :slash_starts_regex + + # arguments shorthand + rule %r/(&)(#{id})?/ do + groups Name::Builtin, Name::Attribute + end + + # switch case + rule %r/[|]|\bcase(?=\s)/, Keyword, :switch_underscore + + rule %r/@/, Name::Variable::Instance + rule %r/[.]{3}/, Punctuation + rule %r/:/, Punctuation + + # keywords + rule %r/#{id}/ do |m| + if self.class.loop_control_keywords.include? m[0] + token Keyword + push :loop_control + next + elsif self.class.keywords.include? m[0] + token Keyword + elsif self.class.constants.include? m[0] + token Name::Constant + elsif self.class.builtins.include? m[0] + token Name::Builtin + elsif self.class.declarations.include? m[0] + token Keyword::Declaration + elsif /^[A-Z]/.match(m[0]) && /[^-][a-z]/.match(m[0]) + token Name::Class + else + token Name::Variable + end + push :id + end + + # punctuation and brackets + rule %r/\](?=[!?.]|#{id})/, Punctuation, :id + rule %r/[{(\[;,]/, Punctuation, :slash_starts_regex + rule %r/[})\].]/, Punctuation + + # literals + rule %r/#{int_number}[.]#{int}/, Num::Float + rule %r/0x[0-9A-Fa-f]+/, Num::Hex + rule %r/#{int}/, Num::Integer + + # strings + rule %r/"""/ do + token Str + push do + rule %r/"""/, Str, :pop! + rule %r/"/, Str + mixin :double_strings + end + end + + rule %r/'''/ do + token Str + push do + rule %r/'''/, Str, :pop! + rule %r/'/, Str + mixin :single_strings + end + end + + rule %r/"/ do + token Str + push do + rule %r/"/, Str, :pop! + mixin :double_strings + end + end + + rule %r/'/ do + token Str + push do + rule %r/'/, Str, :pop! + mixin :single_strings + end + end + + # words + rule %r/\\\S[^\s,;\])}]*/, Str + end + + state :code_escape do + rule %r(\\( + c[A-Z]| + x[0-9a-fA-F]{2}| + u[0-9a-fA-F]{4}| + u\{[0-9a-fA-F]{4}\} + ))x, Str::Escape + end + + state :interpolated_expression do + rule %r/}/, Str::Interpol, :pop! + mixin :root + end + + state :interpolation do + # with curly braces + rule %r/[#][{]/, Str::Interpol, :interpolated_expression + # without curly braces + rule %r/(#)(#{id})/ do |m| + groups Str::Interpol, if self.class.builtins.include? m[2] then Name::Builtin else Name::Variable end + end + end + + state :whitespace do + # white space and loop labels + rule %r/(\s+?)(?:^([^\S\n]*)(:#{id}))?/m do + groups Text, Text, Name::Label + end + end + + state :whitespace_single_line do + rule %r([^\S\n]+), Text + end + + state :slash_starts_regex do + mixin :comments + mixin :whitespace + mixin :multiline_regex_begin + + rule %r( + /(\\.|[^\[/\\\n]|\[(\\.|[^\]\\\n])*\])+/ # a regex + ([gimy]+\b|\B) + )x, Str::Regex, :pop! + + rule(//) { pop! } + end + + state :multiline_regex_begin do + rule %r(//) do + token Str::Regex + goto :multiline_regex + end + end + + state :multiline_regex_end do + rule %r(//([gimy]+\b|\B)), Str::Regex, :pop! + end + + state :multiline_regex do + mixin :multiline_regex_end + mixin :regex_comment + mixin :interpolation + mixin :code_escape + rule %r/\\\D/, Str::Escape + rule %r/\\\d+/, Name::Variable + rule %r/./m, Str::Regex + end + + state :regex_comment do + rule %r/^#(\s+.*)?$/, Comment::Single + rule %r/(\s+)(#)(\s+.*)?$/ do + groups Text, Comment::Single, Comment::Single + end + end + + state :comments do + rule %r(/\*.*?\*/)m, Comment::Multiline + rule %r/#.*$/, Comment::Single + end + + state :switch_underscore do + mixin :whitespace_single_line + rule %r/_(?=\s*=>|\s+then\b)/, Keyword + rule(//) { pop! } + end + + state :loop_control do + mixin :whitespace_single_line + rule %r/#{id}(?=[);\n])/, Name::Label + rule(//) { pop! } + end + + state :id do + rule %r/[!?]|[.](?!=)/, Punctuation + rule %r/[{]/ do + # destructuring + token Punctuation + push do + rule %r/[,;]/, Punctuation + rule %r/#{id}/, Name::Attribute + rule %r/#{int}/, Num::Integer + mixin :whitespace + rule %r/[}]/, Punctuation, :pop! + end + end + rule %r/#{id}/, Name::Attribute + rule %r/#{int}/, Num::Integer + rule(//) { goto :slash_starts_regex } + end + + state :strings do + # all strings are multi-line + rule %r/[^#\\'"]+/m, Str + mixin :code_escape + rule %r/\\./, Str::Escape + rule %r/#/, Str + end + + state :double_strings do + rule %r/'/, Str + mixin :interpolation + mixin :strings + end + + state :single_strings do + rule %r/"/, Str + mixin :strings + end + end + end +end diff --git a/spec/lexers/livescript_spec.rb b/spec/lexers/livescript_spec.rb new file mode 100644 index 0000000000..7f59b14c05 --- /dev/null +++ b/spec/lexers/livescript_spec.rb @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- # +# frozen_string_literal: true + +describe Rouge::Lexers::Livescript do + let(:subject) { Rouge::Lexers::Livescript.new } + + describe 'guessing' do + include Support::Guessing + + it 'guesses by filename' do + assert_guess :filename => 'foo.ls' + end + + it 'guesses by mimetype' do + assert_guess :mimetype => 'text/livescript' + end + + it 'guesses by source' do + assert_guess :source => '#!/usr/bin/env lsc' + end + end +end diff --git a/spec/visual/samples/livescript b/spec/visual/samples/livescript new file mode 100644 index 0000000000..b3996f4a2f --- /dev/null +++ b/spec/visual/samples/livescript @@ -0,0 +1,304 @@ +mitch = + age: 21 + height: 180cm + pets: [\dog, \goldfish] + +phile = {} +phile{height, pets} = mitch +phile.height #=> 180 +phile.pets #=> ['dog', 'goldfish'] + +a = [2 7 1 8] + ..push 3 + ..shift! + ..sort! +a #=> [1,3,7,8] + +# Numbers +const PI = 3.14 +2~1000 +16~ff +10e-15 +5_000m == 5km * 1e3m +0xAAff + +# Some operators +n = n++ - ++n - n-- + --n + +14 .&. 9 #=> 8 +14 .|. 9 #=> 15 +14 .^. 9 #=> 7 +~9 #=> -10 +9 .<<. 2 #=> 36 +-9 .>>. 2 #=> -3 +-9 .>>>. 2 #=> 1073741821 + +2 < 4 +9 > 7 +8 <= 8 +7 >= 8 + +4 >? 8 #=> 8 +9 - 5 4 + +on and off +on && off + +yes or no +yes || no + +not false +!false + +false xor true +false xor false +1 xor 0 +1 xor 1 + +(f and g or h) 3 4 + +3 in list +\id of id: 23, name: \rogers + +# Pipes +[1 2 3] |> reverse +reverse <| [1 2 3] +4 +|> (+ 1) +|> even + +# Function calls +add-two-times-two = (+ 2) >> (* 2) + +r = +...[4 5 6] +t = typeof! ...[\b 5 {}] +c = ~...[4, 5] +++...player<[strength hp]> +i = new ...[some, classes] +c = ^^...[copy, these, {}] +delete ...list[1, 2, 3] +do ...[a, b, c] + +# arguments shorthand & +-> &1 + &2 || &a + &b || & + +# Imports +obj = {one: 1, two: 2} +obj <<< three: 3 +{go: true} <<<< window +import obj +obj2 = ^^obj +obj3 = obj with three: 3 + +export func = -> + +require! <[ fs path ]> +require! jQuery: $ +require! { + fs: filesystem + 'prelude-ls': {map, id} + path: {join, resolve}:p +} + +{ property : 1 } + +# These shouldn't be highlighted as constants, since this is +# totally legit in livescript +{ true: 1, false: 0 } +foo.true = foo.false = @undefined = @null +{ if: 0, try: 1, in: 2 } +{ and: 0, or: 1, xor: 2, not: 3, is: 4, isnt: 5 } +foo.instanceof = foo.typeof = @finally = @super + +# These should +{ 1: true, 0: false } +1 instanceof Number +typeof \foo + +# These refer to builtins +window.foo = bar(Array) + +# These are properties +foo.window = bar.Array + +# A more complicated comment with some `code` inside +# ['TAG', 'value', line] ++ [column] +# +# [Jison](http://github.com/zaach/jison) generated [parser](../lib/parser.js). +# { 1: true, 0: false } +# 1 instanceof Number +# typeof \foo + +/* +A more complicated comment with some `code` inside +['TAG', 'value', line] ++ [column] + +[Jison](http://github.com/zaach/jison) generated [parser](../lib/parser.js). +{ 1: true, 0: false } +1 instanceof Number +typeof \foo +*/ + +# Some interpolated values +a-function = ({x = 1, y = 3} = {}) -> "#{\#}#x#{\,}#{y}" + +function detab str, len + if len then str.replace detab[len]||=//\n[^\n\S]{1,#len}//g '\n' else str + +# Erases all newlines and indentations. +unlines = (.replace /\n[^\n\S]*/g '') + +# Multiline regex + +symbol = // + [-/^]= | [%+:*]{1,2}= | \|>= # compound assign +| \.(?:[&\|\^] | << | >>>?)\.=? # bitwise and shifts +| \.{1,3} # dot / cascade / splat/placeholder/yada*3 +| \^\^ # clone +| \*?<(?:--?|~~?)!? # backcall +| !?(?:--?|~~?)>\*? # function, bound function +| ([-+&|:])\1 # crement / logic / `prototype` +| %% # mod +| & # arguments +| \([^\n\S]*\) # call +| [!=]==? # strict equality, deep equals +| !?\~= # fuzzy equality +| @@? # this / constructor +| <\[(?:[\s\S]*?\]>)? # words +| <<<]== | <<= | >>= # deep {less,greater}-than-(or-equal-to) +| << | >> # compose +| [<>]\??=? # {less,greater}-than-(or-equal-to) / min/max +| \|> # pipe +| \| # case +| => # then +| \*\* | \^ # pow +| ` # backticks +| [^\s#]? +//g + +# With some hex numbers and unicode +// + ( (?!\s)[$\xAA-\uFFDC] ) +//ig + +# Some single line regexes +/[^\n\S]*(?:#.*)?/gimy +multident = /(?:\s*#.*)*(?:\n([^\n\S]*))*/g +simplestr = /'[^\\']*(?:\\[\s\S][^\\']*)*'|/g + +# list of words +<[ , : -> else { assign } ]> ++ <[ + do [ generator ] = ... | ! enum ( interface ) + package private protected public static +]> + +js-keywords = <[ + true false null this void super return throw break continue + if else for while switch case default try catch finally + function class extends implements new do delete typeof in instanceof + let with var const import export debugger yield +]> + +exports <<< + + lex: ( + code + options + ) -> (^^exports).tokenize code||'' options||{} + + rewrite: (it || @tokens) -> + first-pass it + it.shift! if it.0?.0 is 'NEWLINE' + it + + tokenize: (code, o) -> + @inter or code.=replace /[\r\u2028\u2029\uFEFF]/g '' + code = '\n' + code + while c = code.char-at i + if @variable-x + then throw new Error 'Error' + +function ok token, i + switch tag = token.0 + | \, => break + | \NEWLINE => return true if inline + | \DEDENT => return true + | \POST_IF \FOR \WHILE => return inline + | _ => return false + t1 = tokens[i+1]?.0 + t1 is not (if tag is \, then \NEWLINE else \COMMENT) and + \: is not tokens[if t1 is \( then 1 + index-of-pair tokens, i+1 else i+2]?.0 + +!function go token, i then tokens.splice i, 0 ['}' '' token.2, token.3] + +switch id +case <[ true false on off yes no null void arguments debugger ]> + tag = 'LITERAL' +case <[ new do typeof delete ]> + tag = 'UNARY' +case 'yield' + tag = 'YIELD' +case 'return' 'throw' + tag = 'HURL' +case 'break' 'continue' + tag = 'JUMP' +case 'this' 'eval' 'super' + return @token 'LITERAL' id, true .length +case 'in' 'of' + if @fget 'for' + @fset 'for' false + if id is 'in' + @fset 'by' true + id = '' + if last.0 is 'ID' and @tokens[*-2].0 in <[ , ] } ]> + id = @tokens.pop!1 + @tokens.pop! if @tokens[*-1].0 is ',' + break + fallthrough +case 'and' 'or' 'xor' 'is' 'isnt' + @unline! + tag = if id in <[ is isnt ]> then 'COMPARE' else 'LOGIC' + tag = 'BIOP' if last.0 is '(' + @token tag, switch id + | 'is' => '===' + | 'isnt' => '!==' + | 'or' => '||' + | 'and' => '&&' + | 'xor' => 'xor' + @last.alias = true + return id.length +case otherwise 0 + +if rnum is /[0-9]/ + @carp "invalid number base #radix (with number #rnum), + base must be from 2 to 36" + +@validate /^(?:[gimy]{1,4}|[?$]?)/.exec(rest).0 + +do-line = (code, index) -> + [input, tabs] = (MULTIDENT <<< last-index: index).exec code + {length} = @count-lines input + {last} = this + last <<< {+eol, +spaced} + return length if index + length >= code.length + if tabs and (@emender ||= //[^#{ tabs.char-at! }]//).exec tabs + @carp "contaminated indent #{ escape that }" + if 0 > delta = tabs.length - @dent + @dedent -delta + @newline! + else + [tag, val] = last + if tag is 'ASSIGN' and val + '' not in <[ = := += ]> + or val is '++' and @tokens[*-2].spaced + or tag in <[ +- PIPE BACKPIPE COMPOSE DOT LOGIC MATH COMPARE RELATION + SHIFT IN OF TO BY FROM EXTENDS IMPLEMENTS ]> + return length + if delta then @indent delta else @newline! + @fset 'for' false + @fset 'by' false + length + +try Function "'use strict'; var #id" catch + @carp "invalid variable interpolation '#id'"