///|
type LexerReporter = (String, String) -> Unit raise ParseFailure
///|
priv struct Lexer {
input : String
mut offset : Int
catcodes : Map[String, Int]
report_nonstrict : LexerReporter
}
///|
fn Lexer::make(
input : String,
report_nonstrict? : LexerReporter = (_code, _message) => (),
) -> Lexer {
let lexer : Lexer = { input, offset: 0, catcodes: Map([]), report_nonstrict, }
lexer.set_catcode("%", 14)
lexer.set_catcode("~", 13)
lexer
}
///|
fn Lexer::set_catcode(self : Lexer, char : String, code : Int) -> Unit {
self.catcodes[char] = code
}
///|
fn Lexer::catcode(self : Lexer, char : String) -> Int? {
self.catcodes.get(char)
}
///|
fn is_ascii_alphabetic(c : UInt16) -> Bool {
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
///|
fn is_combining_diacritical_mark(c : UInt16) -> Bool {
c >= 0x0300 && c <= 0x036f
}
///|
fn is_js_line_terminator(c : UInt16) -> Bool {
c == '\n' || c == '\r' || c == 0x2028 || c == 0x2029
}
///|
fn verb_end(input : String, start : Int, starred : Bool) -> Int? {
let prefix = if starred { "\\verb*" } else { "\\verb" }
if !input[start:].has_prefix(prefix[:]) {
return None
}
let delimiter_offset = start + prefix.length()
if delimiter_offset >= input.length() {
return None
}
let delimiter = input[delimiter_offset]
if !starred && (delimiter == '*' || is_ascii_alphabetic(delimiter)) {
return None
}
for i = delimiter_offset + 1; i < input.length(); i = i + 1 {
let c = input[i]
if c == delimiter {
break Some(i + 1)
} else if is_js_line_terminator(c) {
break None
}
} nobreak {
None
}
}
///|
fn next_line_start(input : String, offset : Int) -> Int? {
for i = offset; i < input.length(); i = i + 1 {
if input[i] == '\n' {
break Some(i + 1)
}
} nobreak {
None
}
}
///|
fn unexpected_character(input : String, offset : Int) -> ParseFailure {
let text = input.unsafe_substring(start=offset, end=offset + 1)
UnexpectedCharacter(
message="Unexpected character: '\{text}'",
loc=Some(SourceLocation::make(input, start=offset, end=offset + 1)),
)
}
///|
fn Lexer::match_token(self : Lexer) -> Token raise ParseFailure {
let input = self.input
let start = self.offset
// A verb delimiter can leave the cursor inside a UTF-16 surrogate pair.
// Reject that position instead of snapping it forward with a clamped slice.
guard input.get_view(start~) is Some(remaining) else {
raise unexpected_character(input, start)
}
// The closing verb delimiter depends on the input and needs a manual scan.
let (text, end) = if verb_end(input, start, true) is Some(end) {
(input.unsafe_substring(start~, end~), end)
} else if verb_end(input, start, false) is Some(end) {
(input.unsafe_substring(start~, end~), end)
} else {
// Preserve KaTeX's ordered alternatives, all anchored at the current offset.
lexmatch remaining {
(re"^[ \r\n\t]+", after=rest) => (" ", rest.start_offset())
(re"^\\(\n|[ \r\t]+\n?)[ \r\t]*", after=rest) =>
("\\ ", rest.start_offset())
(
re"^[\u{0021}-\u{005b}\u{005d}-\u{2027}\u{202a}-\u{d7ff}\u{f900}-\u{10ffff}][\u{0300}-\u{036f}]*" as text,
after=rest,
) => (text.to_owned(), rest.start_offset())
((re"^\\[a-zA-Z@]+" as text) + re"[ \r\n\t]*", after=rest) =>
(text.to_owned(), rest.start_offset())
(re"^\\[\u{0000}-\u{d7ff}\u{e000}-\u{ffff}]" as text, after=rest) =>
(text.to_owned(), rest.start_offset())
_ => raise unexpected_character(input, start)
}
}
self.offset = end
Token::make(text, loc=SourceLocation::make(input, start~, end~))
}
///|
fn Lexer::lex(self : Lexer) -> Token raise ParseFailure {
for ;; {
if self.offset == self.input.length() {
break Token::eof(self.input, self.offset)
}
let token = self.match_token()
if self.catcodes.get(token.text) == Some(14) {
match next_line_start(self.input, self.offset) {
Some(offset) => {
self.offset = offset
continue
}
None => {
self.offset = self.input.length()
(self.report_nonstrict)(
"commentAtEnd", "% comment has no terminating newline; LaTeX would fail because of commenting the end of math mode (e.g. $)",
)
continue
}
}
} else {
break token
}
}
}