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

Initial Gleam support #2070

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ Other contributors, listed alphabetically, are:
* Jesper Noehr -- HTML formatter "anchorlinenos"
* Mike Nolta -- Julia lexer
* Jonas Obrist -- BBCode lexer
* Antonio Ognio -- Gleam lexer
* David Oliva -- Rebol lexer
* Jon Parise -- Protocol buffers lexer
* Ronny Pfannschmidt -- BBCode lexer
Expand Down
188 changes: 188 additions & 0 deletions lib/rouge/lexers/gleam.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# -*- coding: utf-8 -*- #
# frozen_string_literal: true

module Rouge
module Lexers
# Lexer for the Gleam programming language (https://gleam.run/)
class Gleam < RegexLexer
title 'Gleam'
desc 'The Gleam programming language (https://gleam.run/)'
tag 'gleam'
filenames '*.gleam'
mimetypes 'text/x-gleam'

# Character sets
ID = /[a-z_][a-zA-Z0-9_]*/.freeze
TYPE_ID = /[A-Z][a-zA-Z0-9_]*/.freeze
MODULE_METHOD_CALL = %r{([a-z_][a-zA-Z0-9_]*)(\.)([a-zA-Z_][a-zA-Z0-9_]*)}.freeze
WHITESPACE = /\s+/.freeze
NEWLINE = /\n/.freeze

# Keywords, built-ins, constants
KEYWORDS_LIST = %w[
as assert case const external fn if import let opaque pub todo try type use
module else panic test
].freeze

KEYWORDS = %r{\b(?:#{KEYWORDS_LIST.join('|')})\b}.freeze

BUILTINS_LIST = %w[
Int Float Bool String List Result Option Iterator
].freeze

BUILTINS = %r{\b(?:#{BUILTINS_LIST.join('|')})\b}.freeze

CONSTANTS = %r{\b(?:Nil|Ok|Error|Stop|Continue|True|False)\b}.freeze

BIT_STRING_KEYWORDS_LIST = %w[
binary bits bytes int float bit_string bit_array utf8 utf16 utf32
utf8_codepoint utf16_codepoint utf32_codepoint signed unsigned big little
native unit size
].freeze

BIT_STRING_KEYWORDS = %r{\b(?:#{BIT_STRING_KEYWORDS_LIST.join('|')})\b}.freeze

# Operators and punctuation
OPERATORS = %r{>>|<<|\|\||&&|==|!=|<=|>=|->|=>|<-|<>|\|>|[-+\/*%=!<>&|^~]}.freeze
PUNCTUATION = /[()\[\]{}.,:;]/.freeze

# Numbers
BINARY_NUMBER = /\b0b[01](?:_?[01]+)*\b/.freeze
OCTAL_NUMBER = /\b0o[0-7](?:_?[0-7]+)*\b/.freeze
HEX_NUMBER = /\b0x[0-9a-fA-F](?:_?[0-9a-fA-F]+)*\b/.freeze
FLOAT_NUMBER = /\b\d[\d_]*\.\d[\d_]*(e[+-]?\d[\d_]*)?\b/.freeze
INTEGER_NUMBER = /\b\d[\d_]*\b/.freeze

# Strings
DOUBLE_QUOTED_STRING = %r{"(\\\\|\\"|[^"])*"}.freeze
SINGLE_QUOTED_STRING = %r{'(\\\\|\\'|[^'])*'}.freeze
ESCAPE_SEQUENCE = %r{\\[nrt\\"'0]}.freeze

# Comments
LINE_COMMENT = %r{//.*?$}.freeze

state :root do
mixin :simple_tokens

# Raw strings (backticks)
rule %r{`}, Str::Backtick, :raw_string

# Triple-quoted strings
rule %r{"""}, Str::Double, :triple_string

# Double-quoted strings
rule %r{"}, Str::Double, :string

# Single-quoted strings (characters)
rule %r{'}, Str::Char, :char

# Bit arrays
rule %r{<<}, Operator, :bitarray
end

state :simple_tokens do
# Whitespace and newline
rule WHITESPACE, Text::Whitespace
rule NEWLINE, Text

# Comments
rule LINE_COMMENT, Comment::Single

# Keywords, built-ins, constants
rule KEYWORDS, Keyword
rule BUILTINS, Name::Builtin
rule CONSTANTS, Name::Constant

# Type names (user-defined)
rule %r{\b#{TYPE_ID}\b}, Name::Class

# Function definitions
rule %r{(\b(?:pub\s+)?fn\b)(\s+)(#{ID}) } do
groups Keyword, Text::Whitespace, Name::Function
end

# Module and method calls (e.g., list.map)
rule MODULE_METHOD_CALL do
groups Name::Namespace, Punctuation, Name::Function
end

# Function calls
rule %r{(#{ID})(\s*)(\() } do
groups Name::Function, Text::Whitespace, Punctuation
push :func_call_params
end

# Module-qualified function calls
rule %r{(#{ID})(\.)(#{ID})(\s*)(\() } do
groups Name::Namespace, Punctuation, Name::Function, Text::Whitespace, Punctuation
push :func_call_params
end

# Identifiers (variables, fields)
rule %r{\b#{ID}\b}, Name::Variable

# Discard names (e.g., _var)
rule %r{\b_[a-z][a-zA-Z0-9_]*\b}, Name::Builtin::Pseudo

# Operators and punctuation
rule OPERATORS, Operator
rule PUNCTUATION, Punctuation

# Numbers
rule BINARY_NUMBER, Num::Bin
rule OCTAL_NUMBER, Num::Oct
rule HEX_NUMBER, Num::Hex
rule FLOAT_NUMBER, Num::Float
rule INTEGER_NUMBER, Num::Integer

# Strings and escape sequences
rule ESCAPE_SEQUENCE, Str::Escape

# Attributes
rule %r{[@]#{ID}}, Name::Decorator
end

# Function call parameters
state :func_call_params do
rule %r{\)}, Punctuation, :pop!
rule %r{[^)]+}, Text
end

# Raw strings
state :raw_string do
rule %r{[^`]+}, Str::Backtick
rule %r{`}, Str::Backtick, :pop!
end

# Triple-quoted strings
state :triple_string do
rule %r{"""}, Str::Double, :pop!
rule %r{[^"]+}, Str::Double
rule %r{"}, Str::Double
end

# Single-line strings
state :string do
rule %r{[^"\\]+}, Str::Double
rule %r{\\[\\"]}, Str::Escape
rule %r{"}, Str::Double, :pop!
end

# Character literals
state :char do
rule %r{[^'\\]+}, Str::Char
rule %r{\\[\\']}, Str::Escape
rule %r{'}, Str::Char, :pop!
end

# Bit arrays
state :bitarray do
rule %r{>>}, Operator, :pop!
rule WHITESPACE, Text::Whitespace
rule BIT_STRING_KEYWORDS, Keyword
rule %r{[^>]+}, Text
end
end
end
end

74 changes: 74 additions & 0 deletions spec/lexers/gleam_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# frozen_string_literal: true

describe Rouge::Lexers::Gleam do
let(:subject) { Rouge::Lexers::Gleam.new }

describe 'guessing' do
include Support::Guessing

it 'guesses by filename' do
assert_guess(filename: 'example.gleam')
end

it 'guesses by mimetype' do
assert_guess(mimetype: 'text/x-gleam')
end
end

describe 'lexing keywords' do
it 'recognizes keywords' do
%w[let fn import pub case of type as if else try opaque assert todo async await].each do |keyword|
expect(subject.lex(keyword).to_a).to include([:keyword, keyword])
end
end
end

describe 'lexing built-in types' do
it 'recognizes built-in types' do
%w[Int Float Bool String List Nil Result Option Error Ok].each do |builtin|
expect(subject.lex(builtin).to_a).to include([:keyword_type, builtin])
end
end
end

describe 'lexing constants' do
it 'recognizes constants' do
%w[Nil Ok Error Stop Continue True False].each do |constant|
expect(subject.lex(constant).to_a).to include([:keyword_constant, constant])
end
end
end

describe 'lexing numbers' do
it 'recognizes integers' do
expect(subject.lex('42').to_a).to include([:num_integer, '42'])
end

it 'recognizes floating-point numbers' do
expect(subject.lex('3.14').to_a).to include([:num_float, '3.14'])
end

it 'recognizes hexadecimal numbers' do
expect(subject.lex('0x1A3F').to_a).to include([:num_hex, '0x1A3F'])
end
end

describe 'lexing strings' do
it 'recognizes double-quoted strings' do
expect(subject.lex('"Hello, Gleam!"').to_a).to include([:str_double, '"Hello, Gleam!"'])
end
end

describe 'lexing module and method calls' do
it 'recognizes module and method calls' do
expect(subject.lex('list.map').to_a).to include([:name_namespace, 'list'], [:punctuation, '.'], [:name_function, 'map'])
end
end

describe 'lexing operators' do
it 'recognizes pipeline operator' do
expect(subject.lex('|>').to_a).to include([:operator, '|>'])
end
end
end
Loading