///|
/// One lexed tag occurrence.
///
/// `name` is the upper-cased tag name, with a leading `/` for a closing tag and
/// without attributes, so `` and `` are told apart the same
/// way every time. `value` is the text between this tag and the next one, still
/// entity-encoded. `line` is the 1-based line of the tag's `<`. `self_closing`
/// marks an empty element such as ``: it has no value and no closing tag,
/// so it must not open a scope that nothing will ever close.
priv struct TagToken {
name : String
value : String
line : Int
self_closing : Bool
}
///|
/// Whether a raw tag body ends with the self-closing marker, as in `` or
/// ``.
///
/// The check looks at the last non-space character, so an attribute value that
/// merely contains a slash (``) is not mistaken for an empty
/// element.
fn is_self_closing(raw : String) -> Bool {
let trimmed = raw.trim_end()
if trimmed.length() == 0 {
false
} else {
trimmed.get_char(trimmed.length() - 1) is Some('/')
}
}
///|
fn is_space(c : Char) -> Bool {
c == ' ' || c == '\t' || c == '\n' || c == '\r'
}
///|
/// Whether `literal` occurs in `chars` at `index`.
fn match_chars(chars : Array[Char], index : Int, literal : Array[Char]) -> Bool {
if index + literal.length() > chars.length() {
return false
}
for offset in 0.. String {
let chars = raw.iter().to_array()
let name = StringBuilder()
let mut index = 0
while index < chars.length() && is_space(chars[index]) {
index += 1
}
if index < chars.length() && chars[index] == '/' {
name.write_char('/')
index += 1
while index < chars.length() && is_space(chars[index]) {
index += 1
}
}
while index < chars.length() && !is_space(chars[index]) && chars[index] != '/' {
name.write_char(chars[index].to_ascii_uppercase())
index += 1
}
name.to_string()
}
///|
/// Whether `value` is exactly `count` ASCII digits.
fn is_digits(value : String, count : Int) -> Bool {
let chars = value.iter().to_array()
if chars.length() != count {
return false
}
for c in chars {
if c < '0' || c > '9' {
return false
}
}
true
}
///|
/// Parse a decimal integer with an optional sign, or return `None` when the
/// text is not one or does not fit in an `Int`.
///
/// OFX writes small integers such as a status code as text, and a status code
/// may be negative, so the sign is part of the value rather than an error.
fn parse_int(text : String) -> Int? {
let chars = text.iter().to_array()
if chars.length() == 0 {
return None
}
let mut index = 0
let mut negative = false
if chars[0] == '-' || chars[0] == '+' {
negative = chars[0] == '-'
index = 1
}
if index >= chars.length() {
return None
}
let mut value = 0
while index < chars.length() {
let c = chars[index]
if c < '0' || c > '9' {
return None
}
let digit = c.to_int() - 48
if value > (2147483647 - digit) / 10 {
return None
}
value = value * 10 + digit
index += 1
}
Some(if negative { -value } else { value })
}
///|
/// The aggregate a closing tag refers to, or `None` when the tag is not a
/// closing tag.
fn close_target(tag : String) -> String? {
let chars = tag.iter().to_array()
if chars.length() == 0 || chars[0] != '/' {
return None
}
let out = StringBuilder()
for position in 1..`
/// is reported and scanning stops, because every later offset would be a guess.
fn lex(source : String, diagnostics : Array[Diagnostic]) -> Array[TagToken] {
let chars = source.iter().to_array()
let comment_open = "".iter().to_array()
let cdata_open = "".iter().to_array()
let pi_open = "".iter().to_array()
let pi_close = "?>".iter().to_array()
let declaration_open = "' {
if chars[index] == '\n' {
line += 1
}
index += 1
}
if index >= chars.length() {
diagnostics.push(error_at("unclosed-tag", "@" + open_line.to_string()))
break
}
index += 1
continue
}
let tag_line = line
index += 1
let raw = StringBuilder()
let mut in_double_quote = false
let mut in_single_quote = false
let mut closed = false
while index < chars.length() {
let c = chars[index]
if in_double_quote {
if c == '"' {
in_double_quote = false
}
raw.write_char(c)
index += 1
continue
}
if in_single_quote {
if c == '\'' {
in_single_quote = false
}
raw.write_char(c)
index += 1
continue
}
if c == '>' {
closed = true
index += 1
break
}
if c == '<' {
// A `<` that is not inside quotes ends the tag body: report the tag as
// unclosed and let the outer loop start over at this `<`.
break
}
if c == '"' {
in_double_quote = true
} else if c == '\'' {
in_single_quote = true
} else if c == '\n' {
line += 1
}
raw.write_char(c)
index += 1
}
if !closed {
diagnostics.push(error_at("unclosed-tag", "@" + tag_line.to_string()))
if index >= chars.length() {
break
}
continue
}
let raw_text = raw.to_string()
let name = tag_name(raw_text)
if name == "" {
diagnostics.push(error_at("empty-tag", "@" + tag_line.to_string()))
continue
}
let value = StringBuilder()
let run = StringBuilder()
let run_line = line
while index < chars.length() && chars[index] != '<' {
if chars[index] == '\n' {
line += 1
}
run.write_char(chars[index])
index += 1
}
decode_into(value, run.to_string(), run_line, diagnostics)
while index < chars.length() && match_chars(chars, index, cdata_open) {
index += cdata_open.length()
let mut cdata_closed = false
while index < chars.length() {
if match_chars(chars, index, cdata_close) {
index += cdata_close.length()
cdata_closed = true
break
}
if chars[index] == '\n' {
line += 1
}
value.write_char(chars[index])
index += 1
}
if !cdata_closed {
diagnostics.push(
error_at("unclosed-cdata", "@" + open_line.to_string()),
)
break
}
let tail = StringBuilder()
let tail_line = line
while index < chars.length() && chars[index] != '<' {
if chars[index] == '\n' {
line += 1
}
tail.write_char(chars[index])
index += 1
}
decode_into(value, tail.to_string(), tail_line, diagnostics)
}
tokens.push({
name,
value: value.to_string().trim().to_owned(),
line: tag_line,
self_closing: is_self_closing(raw_text),
})
}
tokens
}