diff --git a/src/just.gleam b/src/just.gleam
index 583764c..0802b77 100644
--- a/src/just.gleam
+++ b/src/just.gleam
@@ -9,6 +9,7 @@ pub opaque type Lexer {
ignore_whitespace: Bool,
strict_mode: Bool,
mode: LexerMode,
+ errors: List(Error),
)
}
@@ -17,6 +18,14 @@ type LexerMode {
TreatSlashAsDivision
}
+pub type Error {
+ UnknownCharacter(character: String)
+ UnterminatedString
+ UnterminatedComment
+ UnterminatedRegularExpression
+ UnterminatedTemplate
+}
+
pub fn new(source: String) -> Lexer {
Lexer(
source:,
@@ -24,6 +33,7 @@ pub fn new(source: String) -> Lexer {
ignore_whitespace: False,
strict_mode: False,
mode: TreatSlashAsRegex,
+ errors: [],
)
}
@@ -39,46 +49,11 @@ pub fn strict_mode(lexer: Lexer) -> Lexer {
Lexer(..lexer, strict_mode: True)
}
-pub fn tokenise(lexer: Lexer) -> List(Token) {
+pub fn tokenise(lexer: Lexer) -> #(List(Token), List(Error)) {
let #(lexer, tokens) = maybe_lex_hashbang_comment(lexer)
do_tokenise(lexer, tokens)
}
-fn update_mode_with_token(lexer: Lexer, token: Token) -> Lexer {
- let mode = case token {
- // Comments and whitespace don't affect lexing mode
- token.SingleLineComment(_)
- | token.MultiLineComment(_)
- | token.HashBangComment(_)
- | token.Whitespace(_)
- | token.LineTerminator(_)
- | token.EndOfFile -> lexer.mode
-
- // Values make us look for division
- token.Identifier(_)
- | token.PrivateIdentifier(_)
- | token.Number(_)
- | token.BigInt(_)
- | token.String(..)
- | token.RegularExpression(_)
- | token.TemplateTail(_) -> TreatSlashAsDivision
-
- // These keywords act as values, so we look for division after them
- token.False | token.Null | token.This | token.True -> TreatSlashAsDivision
-
- // After a grouping we look for division
- token.RightParen | token.RightSquare -> TreatSlashAsDivision
-
- // These can be either postfix or prefix. Either way, we keep the lexing mode the same.
- token.DoublePlus | token.DoubleMinus -> lexer.mode
-
- // In any other case, we look for a regular expression next.
- _ -> TreatSlashAsRegex
- }
-
- Lexer(..lexer, mode:)
-}
-
fn maybe_lex_hashbang_comment(lexer: Lexer) -> #(Lexer, List(Token)) {
case lexer.source {
"#!" <> source -> {
@@ -96,9 +71,12 @@ fn maybe_lex_hashbang_comment(lexer: Lexer) -> #(Lexer, List(Token)) {
}
}
-fn do_tokenise(lexer: Lexer, tokens: List(Token)) -> List(Token) {
+fn do_tokenise(lexer: Lexer, tokens: List(Token)) -> #(List(Token), List(Error)) {
case next(lexer) {
- #(_, token.EndOfFile) -> list.reverse([token.EndOfFile, ..tokens])
+ #(lexer, token.EndOfFile) -> #(
+ list.reverse([token.EndOfFile, ..tokens]),
+ list.reverse(lexer.errors),
+ )
#(lexer, token.TemplateHead(_) as token) -> {
let #(lexer, tokens) =
lex_template_parts(
@@ -113,6 +91,13 @@ fn do_tokenise(lexer: Lexer, tokens: List(Token)) -> List(Token) {
}
}
+fn maybe_token(lexer: Lexer, token: Token, condition: Bool) -> #(Lexer, Token) {
+ case condition {
+ True -> #(lexer, token)
+ False -> next(lexer)
+ }
+}
+
fn next(lexer: Lexer) -> #(Lexer, Token) {
case lexer.source {
"" -> #(lexer, token.EndOfFile)
@@ -143,28 +128,22 @@ fn next(lexer: Lexer) -> #(Lexer, Token) {
"\u{000A}" as space <> source
| "\u{000D}" as space <> source
| "\u{2028}" as space <> source
- | "\u{2029}" as space <> source -> {
- let lexer = advance(lexer, source)
- case lexer.ignore_whitespace {
- True -> next(lexer)
- False -> #(lexer, token.LineTerminator(space))
- }
- }
+ | "\u{2029}" as space <> source ->
+ maybe_token(
+ advance(lexer, source),
+ token.LineTerminator(space),
+ !lexer.ignore_whitespace,
+ )
"//" <> source -> {
let #(lexer, contents) = lex_until_end_of_line(advance(lexer, source), "")
- case lexer.ignore_comments {
- True -> next(lexer)
- False -> #(lexer, token.SingleLineComment(contents))
- }
- }
- "/*" <> source -> {
- let #(lexer, contents) = lex_multiline_comment(advance(lexer, source), "")
- case lexer.ignore_comments {
- True -> next(lexer)
- False -> #(lexer, token.MultiLineComment(contents))
- }
+ maybe_token(
+ lexer,
+ token.SingleLineComment(contents),
+ !lexer.ignore_comments,
+ )
}
+ "/*" <> source -> lex_multiline_comment(advance(lexer, source), "")
"0b" as prefix <> source ->
lex_radix_number(advance(lexer, source), 2, prefix, False)
@@ -209,10 +188,8 @@ fn next(lexer: Lexer) -> #(Lexer, Token) {
| ".0" as digit <> source ->
lex_number(advance(lexer, source), digit, Decimal, AfterNumber)
- "/" <> source if lexer.mode == TreatSlashAsRegex -> {
- let #(lexer, value) = lex_regex(advance(lexer, source), "", False)
- #(lexer, token.RegularExpression(value))
- }
+ "/" <> source if lexer.mode == TreatSlashAsRegex ->
+ lex_regex(advance(lexer, source), "", False)
"{" <> source -> #(advance(lexer, source), token.LeftBrace)
"}" <> source -> #(advance(lexer, source), token.RightBrace)
@@ -344,23 +321,38 @@ fn next(lexer: Lexer) -> #(Lexer, Token) {
#(lexer, token)
}
- "'" as quote <> source | "\"" as quote <> source -> {
- let #(lexer, string) = lex_string(advance(lexer, source), quote, "")
- #(lexer, token.String(quote, string))
- }
+ "'" as quote <> source | "\"" as quote <> source ->
+ lex_string(advance(lexer, source), quote, "")
"`" <> source -> lex_template_head(advance(lexer, source), "")
- _ -> #(lexer, token.EndOfFile)
+ _ ->
+ case string.pop_grapheme(lexer.source) {
+ Error(_) -> #(lexer, token.EndOfFile)
+ Ok(#(character, source)) -> #(
+ lexer |> advance(source) |> error(UnknownCharacter(character)),
+ token.Unknown(character),
+ )
+ }
}
}
-fn lex_multiline_comment(lexer: Lexer, lexed: String) -> #(Lexer, String) {
+fn lex_multiline_comment(lexer: Lexer, lexed: String) -> #(Lexer, Token) {
case lexer.source {
- "*/" <> source -> #(advance(lexer, source), lexed)
+ "*/" <> source ->
+ maybe_token(
+ advance(lexer, source),
+ token.MultiLineComment(lexed),
+ !lexer.ignore_comments,
+ )
_ ->
case string.pop_grapheme(lexer.source) {
- Error(_) -> #(lexer, lexed)
+ Error(_) ->
+ maybe_token(
+ error(lexer, UnterminatedComment),
+ token.MultiLineComment(lexed),
+ !lexer.ignore_comments,
+ )
Ok(#(char, source)) ->
lex_multiline_comment(advance(lexer, source), lexed <> char)
}
@@ -574,16 +566,27 @@ fn lex_radix_number(
}
}
-fn lex_string(lexer: Lexer, quote: String, contents: String) -> #(Lexer, String) {
+fn lex_string(lexer: Lexer, quote: String, contents: String) -> #(Lexer, Token) {
case string.pop_grapheme(lexer.source) {
- Error(_) -> #(lexer, contents)
+ Error(_) -> #(
+ error(lexer, UnterminatedString),
+ token.UnterminatedString(quote:, contents:),
+ )
+ Ok(#("\n", _source)) | Ok(#("\r", _source)) -> #(
+ error(lexer, UnterminatedString),
+ token.UnterminatedString(quote:, contents:),
+ )
+
Ok(#(character, source)) if character == quote -> #(
advance(lexer, source),
- contents,
+ token.String(quote:, contents:),
)
Ok(#("\\", source)) ->
case string.pop_grapheme(source) {
- Error(_) -> #(lexer, contents)
+ Error(_) -> #(
+ error(lexer, UnterminatedString),
+ token.UnterminatedString(quote:, contents:),
+ )
Ok(#(character, source)) ->
lex_string(
advance(lexer, source),
@@ -602,13 +605,19 @@ fn lex_template_head(lexer: Lexer, lexed: String) -> #(Lexer, Token) {
"`" <> source -> #(advance(lexer, source), token.String("`", lexed))
"\\" <> source ->
case string.pop_grapheme(source) {
- Error(_) -> #(lexer, token.String("`", lexed))
+ Error(_) -> #(
+ error(lexer, UnterminatedString),
+ token.UnterminatedString("`", lexed),
+ )
Ok(#(character, source)) ->
lex_template_head(advance(lexer, source), lexed <> "\\" <> character)
}
_ ->
case string.pop_grapheme(lexer.source) {
- Error(_) -> #(lexer, token.String("`", lexed))
+ Error(_) -> #(
+ error(lexer, UnterminatedString),
+ token.UnterminatedString("`", lexed),
+ )
Ok(#(character, source)) ->
lex_template_head(advance(lexer, source), lexed <> character)
}
@@ -643,7 +652,10 @@ fn lex_template_parts(
}
"\\" <> source ->
case string.pop_grapheme(source) {
- Error(_) -> #(lexer, [token.TemplateTail(lexed), ..tokens])
+ Error(_) -> #(error(lexer, UnterminatedTemplate), [
+ token.UnterminatedTemplate(lexed),
+ ..tokens
+ ])
Ok(#(character, source)) ->
lex_template_parts(
advance(lexer, source),
@@ -653,7 +665,10 @@ fn lex_template_parts(
}
_ ->
case string.pop_grapheme(lexer.source) {
- Error(_) -> #(lexer, [token.TemplateTail(lexed), ..tokens])
+ Error(_) -> #(error(lexer, UnterminatedTemplate), [
+ token.UnterminatedTemplate(lexed),
+ ..tokens
+ ])
Ok(#(character, source)) ->
lex_template_parts(
advance(lexer, source),
@@ -690,14 +705,27 @@ fn lex_template_parts(
}
}
-fn lex_regex(lexer: Lexer, lexed: String, in_group: Bool) -> #(Lexer, String) {
+fn lex_regex(lexer: Lexer, lexed: String, in_group: Bool) -> #(Lexer, Token) {
case lexer.source {
- "/" <> source if !in_group -> #(advance(lexer, source), lexed)
+ "/" <> source if !in_group -> #(
+ advance(lexer, source),
+ token.RegularExpression(lexed),
+ )
"[" <> source -> lex_regex(advance(lexer, source), lexed <> "[", True)
"]" <> source -> lex_regex(advance(lexer, source), lexed <> "]", False)
+ "\n" <> _source
+ | "\r" <> _source
+ | "\u{2028}" <> _source
+ | "\u{2029}" <> _source -> #(
+ error(lexer, UnterminatedRegularExpression),
+ token.UnterminatedRegularExpression(lexed),
+ )
"\\" <> source ->
case string.pop_grapheme(source) {
- Error(_) -> #(lexer, lexed)
+ Error(_) -> #(
+ error(lexer, UnterminatedRegularExpression),
+ token.UnterminatedRegularExpression(lexed),
+ )
Ok(#(character, source)) ->
lex_regex(
advance(lexer, source),
@@ -707,7 +735,10 @@ fn lex_regex(lexer: Lexer, lexed: String, in_group: Bool) -> #(Lexer, String) {
}
_ ->
case string.pop_grapheme(lexer.source) {
- Error(_) -> #(lexer, lexed)
+ Error(_) -> #(
+ error(lexer, UnterminatedRegularExpression),
+ token.UnterminatedRegularExpression(lexed),
+ )
Ok(#(character, source)) ->
lex_regex(advance(lexer, source), lexed <> character, in_group)
}
@@ -836,3 +867,42 @@ fn lex_until_end_of_line(lexer: Lexer, lexed: String) -> #(Lexer, String) {
fn advance(lexer: Lexer, source: String) -> Lexer {
Lexer(..lexer, source:)
}
+
+fn update_mode_with_token(lexer: Lexer, token: Token) -> Lexer {
+ let mode = case token {
+ // Comments and whitespace don't affect lexing mode
+ token.SingleLineComment(_)
+ | token.MultiLineComment(_)
+ | token.HashBangComment(_)
+ | token.Whitespace(_)
+ | token.LineTerminator(_)
+ | token.EndOfFile -> lexer.mode
+
+ // Values make us look for division
+ token.Identifier(_)
+ | token.PrivateIdentifier(_)
+ | token.Number(_)
+ | token.BigInt(_)
+ | token.String(..)
+ | token.RegularExpression(_)
+ | token.TemplateTail(_) -> TreatSlashAsDivision
+
+ // These keywords act as values, so we look for division after them
+ token.False | token.Null | token.This | token.True -> TreatSlashAsDivision
+
+ // After a grouping we look for division
+ token.RightParen | token.RightSquare -> TreatSlashAsDivision
+
+ // These can be either postfix or prefix. Either way, we keep the lexing mode the same.
+ token.DoublePlus | token.DoubleMinus -> lexer.mode
+
+ // In any other case, we look for a regular expression next.
+ _ -> TreatSlashAsRegex
+ }
+
+ Lexer(..lexer, mode:)
+}
+
+fn error(lexer: Lexer, error: Error) -> Lexer {
+ Lexer(..lexer, errors: [error, ..lexer.errors])
+}
diff --git a/src/just/highlight.gleam b/src/just/highlight.gleam
index 5e24763..2ae8f66 100644
--- a/src/just/highlight.gleam
+++ b/src/just/highlight.gleam
@@ -22,6 +22,7 @@ pub type Token {
Operator(String)
Comment(String)
Punctuation(String)
+ Other(String)
}
/// Convert a string of JavaScript source code into ansi highlighting.
@@ -55,6 +56,7 @@ pub fn to_ansi(code: String) -> String {
Operator(s) -> ansi.magenta(s)
Comment(s) -> ansi.italic(ansi.gray(s))
Punctuation(s) -> ansi.reset(s)
+ Other(s) -> ansi.reset(s)
}
})
}
@@ -119,6 +121,7 @@ pub fn to_html(code: String) -> String {
acc <> ""
Punctuation(s) ->
acc <> "" <> houdini.escape(s) <> ""
+ Other(s) -> acc <> s
}
})
}
@@ -131,7 +134,8 @@ pub fn to_html(code: String) -> String {
///
pub fn to_tokens(code: String) -> List(Token) {
let lexer = just.new(code)
- do_to_tokens(just.tokenise(lexer), [])
+ let #(tokens, _errors) = just.tokenise(lexer)
+ do_to_tokens(tokens, [])
}
fn do_to_tokens(in: List(t.Token), out: List(Token)) -> List(Token) {
@@ -335,5 +339,15 @@ fn do_to_tokens(in: List(t.Token), out: List(Token)) -> List(Token) {
[t.DoubleAmpersandEqual, ..in] -> do_to_tokens(in, [Operator("&&="), ..out])
[t.DoublePipeEqual, ..in] -> do_to_tokens(in, [Operator("||="), ..out])
[t.DoubleQuestionEqual, ..in] -> do_to_tokens(in, [Operator("??="), ..out])
+
+ [t.Unknown(value), ..] -> do_to_tokens(in, [Other(value), ..out])
+ [t.UnterminatedComment(value), ..] ->
+ do_to_tokens(in, [Comment("/*" <> value), ..out])
+ [t.UnterminatedRegularExpression(value), ..] ->
+ do_to_tokens(in, [Regexp("/" <> value), ..out])
+ [t.UnterminatedString(quote:, contents:), ..] ->
+ do_to_tokens(in, [String(quote <> contents), ..out])
+ [t.UnterminatedTemplate(contents), ..] ->
+ do_to_tokens(in, [String(contents), ..out])
}
}
diff --git a/src/just/token.gleam b/src/just/token.gleam
index 35a3840..a2573b6 100644
--- a/src/just/token.gleam
+++ b/src/just/token.gleam
@@ -141,6 +141,13 @@ pub type Token {
DoubleAmpersandEqual
DoublePipeEqual
DoubleQuestionEqual
+
+ // Invalid tokens
+ Unknown(String)
+ UnterminatedString(quote: String, contents: String)
+ UnterminatedTemplate(String)
+ UnterminatedRegularExpression(String)
+ UnterminatedComment(String)
}
pub type ContextualKeyword {
@@ -315,5 +322,11 @@ pub fn to_source(token: Token) -> String {
DoubleAmpersandEqual -> "&&="
DoublePipeEqual -> "||="
DoubleQuestionEqual -> "??="
+
+ Unknown(value) -> value
+ UnterminatedComment(value) -> "/*" <> value
+ UnterminatedRegularExpression(value) -> "/" <> value
+ UnterminatedString(quote:, contents:) -> quote <> contents
+ UnterminatedTemplate(value) -> value
}
}