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

LiveScript lexer #650

Merged
merged 5 commits into from
Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions 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]
310 changes: 310 additions & 0 deletions 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
pyrmont marked this conversation as resolved.
Show resolved Hide resolved

# 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
pyrmont marked this conversation as resolved.
Show resolved Hide resolved
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
pyrmont marked this conversation as resolved.
Show resolved Hide resolved

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
pyrmont marked this conversation as resolved.
Show resolved Hide resolved
22 changes: 22 additions & 0 deletions 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