An Erlang lexer and syntax highlighter in Gleam

Compare changes

Choose any two refs to compare.

+1 -1
.github/workflows/test.yml
··· 15 15 - uses: erlef/setup-beam@v1 16 16 with: 17 17 otp-version: "28.0.0" 18 - gleam-version: "1.12.0" 18 + gleam-version: "1.13.0" 19 19 rebar3-version: "3" 20 20 # elixir-version: "1" 21 21 - run: gleam deps download
+10
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## v2.2.0 - 2025-11-04 4 + 5 + - Updated the JavaScript FFI code to use the new API. 6 + 7 + - `pearl` now requires Gleam v1.13 or higher. 8 + 9 + ## v2.1.0 - 2025-09-01 10 + 11 + - Improved the performance of lexing comments and escape sequences. 12 + 3 13 ## v2.0.0 - 2025-08-22 4 14 5 15 - Merged the `pearl/token` and `pearl/highlight` modules into one `pearl` module.
+6 -7
README.md
··· 5 5 [![Package Version](https://img.shields.io/hexpm/v/pearl)](https://hex.pm/packages/pearl) 6 6 [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/pearl/) 7 7 8 - Pearl is a lexer and syntax highlighter for Erlang, written in Gleam. 9 - The `pearl` module, based on [`glexer`](https://hexdocs.mp/glexer) and 10 - [`just`](https://hexdocs.mp/just), exposes a 11 - standard lexer API, allowing you to convert Erlang source code into tokens. 12 - The `pearl/highlight` module allows you to highlight Erlang code using ansi 13 - colours, html or a custom format. Heavily inspired by [`contour`](https://hexdocs.pm/contour). 8 + Pearl is a lexer and syntax highlighter for Erlang, written in Gleam. The lexer 9 + is based on [`glexer`](https://hexdocs.pm/glexer) and [`just`](https://hexdocs.pm/just), 10 + allowing you to convert Erlang source code into tokens. There is also an API 11 + which allows you to highlight Erlang code using ansi colours, html or a custom 12 + format. Heavily inspired by [`contour`](https://hexdocs.pm/contour). 14 13 15 14 ```sh 16 - gleam add pearl@1 15 + gleam add pearl@2 17 16 ``` 18 17 19 18 ```gleam
+7 -7
birdie_snapshots/highlight_comments_ansi.accepted
··· 14 14 --- 15 15 16 16  17 - %%% This a comment for the entire module 18 - 19 - %% This is a doc comment for the main function 20 - main() -> 21 - % A comment within a function 22 - nil. % This one is after the line 23 -  17 + %%% This a comment for the entire module 18 +  19 + %% This is a doc comment for the main function 20 + main() -> 21 + % A comment within a function 22 +  nil. % This one is after the line 23 + 
+3 -2
gleam.toml
··· 1 1 name = "pearl" 2 - version = "2.0.0" 2 + version = "2.2.0" 3 + gleam = ">= 1.13.0" 3 4 4 5 description = "An Erlang lexer and syntax highlighter for Gleam!" 5 6 licences = ["Apache-2.0"] ··· 10 11 11 12 [dependencies] 12 13 gleam_stdlib = ">= 0.44.0 and < 2.0.0" 13 - splitter = ">= 1.0.0 and < 2.0.0" 14 + splitter = ">= 1.1.0 and < 2.0.0" 14 15 gleam_community_ansi = ">= 1.4.3 and < 2.0.0" 15 16 houdini = ">= 1.2.0 and < 2.0.0" 16 17
+2 -2
manifest.toml
··· 18 18 { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 19 19 { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, 20 20 { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, 21 - { name = "splitter", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "128FC521EE33B0012E3E64D5B55168586BC1B9C8D7B0D0CA223B68B0D770A547" }, 21 + { name = "splitter", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "05564A381580395DCDEFF4F88A64B021E8DAFA6540AE99B4623962F52976AA9D" }, 22 22 { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, 23 23 { name = "trie_again", version = "1.1.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "365FE609649F3A098D1D7FC7EA5222EE422F0B3745587BF2AB03352357CA70BB" }, 24 24 ] ··· 30 30 gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 31 31 houdini = { version = ">= 1.2.0 and < 2.0.0" } 32 32 simplifile = { version = ">= 2.3.0 and < 3.0.0" } 33 - splitter = { version = ">= 1.0.0 and < 2.0.0" } 33 + splitter = { version = ">= 1.1.0 and < 2.0.0" }
+11 -12
src/pearl.gleam
··· 628 628 | "^_" as sequence <> source 629 629 | "^?" as sequence <> source -> #(advance(lexer, source), sequence) 630 630 631 - "x{" <> source -> lex_brace_escape_sequence(advance(lexer, source)) 631 + "x{" <> _source -> lex_brace_escape_sequence(lexer) 632 632 "x" <> source -> lex_hex_escape_sequence(advance(lexer, source)) 633 633 634 634 "0" as char <> source ··· 714 714 } 715 715 716 716 fn lex_brace_escape_sequence(lexer: Lexer) -> #(Lexer, String) { 717 - case splitter.split(lexer.splitters.brace_escape_sequence, lexer.source) { 718 - #(before, "}", after) -> #(advance(lexer, after), "x{" <> before <> "}") 719 - #(before, separator, after) -> #( 720 - advance(error(lexer, UnterminatedEscapeSequence), separator <> after), 721 - "x{" <> before, 722 - ) 717 + case 718 + splitter.split_after(lexer.splitters.brace_escape_sequence, lexer.source) 719 + { 720 + #(before, "") -> #(error(lexer, UnterminatedEscapeSequence), before) 721 + #(before, after) -> #(advance(lexer, after), before) 723 722 } 724 723 } 725 724 ··· 1300 1299 } 1301 1300 1302 1301 fn lex_until_end_of_line(lexer: Lexer) -> #(Lexer, String) { 1303 - let #(before, split, after) = 1304 - splitter.split(lexer.splitters.until_end_of_line, lexer.source) 1305 - #(advance(lexer, split <> after), before) 1302 + let #(before, after) = 1303 + splitter.split_after(lexer.splitters.until_end_of_line, lexer.source) 1304 + #(advance(lexer, after), before) 1306 1305 } 1307 1306 1308 1307 fn lex_whitespace(lexer: Lexer, lexed: String) -> #(Lexer, Token) { ··· 1418 1417 /// pre code .hl-operator { color: #ffaff3 } 1419 1418 /// pre code .hl-string { color: #c8ffa7 } 1420 1419 /// pre code .hl-number { color: #c8ffa7 } 1421 - /// pre code .hl-regexp { color: #c8ffa7 } 1422 - /// pre code .hl-class { color: #ffddfa } 1420 + /// pre code .hl-atom { color: #c8ffa7 } 1421 + /// pre code .hl-module { color: #ffddfa } 1423 1422 /// ``` 1424 1423 /// 1425 1424 /// If you wish to use another format see `to_ansi` or `to_tokens`.
+3 -3
src/pearl_ffi.mjs
··· 1 - import { Ok, Error } from "./gleam.mjs"; 1 + import { Result$Ok, Result$Error } from "./gleam.mjs"; 2 2 3 3 export function strip_prefix(string, prefix) { 4 4 if (string.startsWith(prefix)) { 5 - return new Ok(string.slice(prefix.length)); 5 + return Result$Ok(string.slice(prefix.length)); 6 6 } else { 7 - return new Error(undefined); 7 + return Result$Error(undefined); 8 8 } 9 9 }
+1 -4
test/pearl_test.gleam
··· 221 221 222 222 pub fn unterminated_escape_sequence3_test() { 223 223 let src = "\"\\x{123\"" 224 - assert_errors(src, [ 225 - pearl.UnterminatedEscapeSequence, 226 - pearl.UnterminatedStringLiteral, 227 - ]) 224 + assert_errors(src, [pearl.UnterminatedEscapeSequence]) 228 225 } 229 226 230 227 pub fn unterminated_escape_sequence4_test() {