///|
priv struct XmlTag {
name : String
closing : Bool
self_closing : Bool
attributes : Map[String, String]
input_line : Int
}
///|
priv struct XmlScanner {
chars : Array[Char]
mut index : Int
mut line : Int
}
///|
fn is_xml_space(char : Char) -> Bool {
char == ' ' || char == '\t' || char == '\r' || char == '\n'
}
///|
fn chars_to_string(chars : Array[Char], start : Int, end : Int) -> String {
let output = StringBuilder()
for index in start.. Bool {
let pattern = expected.to_array()
if index + pattern.length() > chars.length() {
return false
}
for offset, char in pattern {
if chars[index + offset] != char {
return false
}
}
true
}
///|
fn XmlScanner::advance(self : XmlScanner, count : Int) -> Unit {
for _ in 0.. Unit raise CoverageError {
while self.index < self.chars.length() &&
!chars_have_prefix(self.chars, self.index, marker) {
self.advance(1)
}
if self.index == self.chars.length() {
raise InvalidCobertura(
start_line,
"unterminated XML section; expected \{marker}",
)
}
self.advance(marker.to_array().length())
}
///|
fn entity_digit(char : Char, radix : Int) -> Int? {
let value = match char {
'0'..='9' => char.to_int() - '0'.to_int()
'a'..='f' => char.to_int() - 'a'.to_int() + 10
'A'..='F' => char.to_int() - 'A'.to_int() + 10
_ => return None
}
if value < radix {
Some(value)
} else {
None
}
}
///|
fn parse_numeric_entity(token : String) -> Char? {
let chars = token.to_array()
if chars.length() < 2 || chars[0] != '#' {
return None
}
let (radix, start) = if chars.length() >= 3 &&
(chars[1] == 'x' || chars[1] == 'X') {
(16, 2)
} else {
(10, 1)
}
if start == chars.length() {
return None
}
let mut value = 0
for index in start.. String raise CoverageError {
let chars = input.to_array()
let output = StringBuilder()
let mut index = 0
while index < chars.length() {
if chars[index] != '&' {
output.write_char(chars[index])
index = index + 1
continue
}
let entity_start = index + 1
let mut end = entity_start
while end < chars.length() && chars[end] != ';' {
end = end + 1
}
if end == chars.length() {
raise InvalidCobertura(input_line, "unterminated XML entity")
}
let token = chars_to_string(chars, entity_start, end)
let decoded = match token {
"amp" => Some('&')
"lt" => Some('<')
"gt" => Some('>')
"quot" => Some('"')
"apos" => Some('\'')
_ => parse_numeric_entity(token)
}
guard decoded is Some(char) else {
raise InvalidCobertura(input_line, "unknown XML entity &\{token};")
}
output.write_char(char)
index = end + 1
}
output.to_string()
}
///|
fn parse_xml_tag(
content : String,
input_line : Int,
) -> XmlTag raise CoverageError {
let chars = content.to_array()
let mut index = 0
while index < chars.length() && is_xml_space(chars[index]) {
index = index + 1
}
let closing = index < chars.length() && chars[index] == '/'
if closing {
index = index + 1
while index < chars.length() && is_xml_space(chars[index]) {
index = index + 1
}
}
let name_start = index
while index < chars.length() &&
!is_xml_space(chars[index]) &&
chars[index] != '/' {
index = index + 1
}
if name_start == index {
raise InvalidCobertura(input_line, "XML tag name must not be empty")
}
let name = chars_to_string(chars, name_start, index)
let attributes : Map[String, String] = Map([])
let mut self_closing = false
while index < chars.length() {
while index < chars.length() && is_xml_space(chars[index]) {
index = index + 1
}
if index == chars.length() {
break
}
if chars[index] == '/' {
self_closing = true
index = index + 1
while index < chars.length() && is_xml_space(chars[index]) {
index = index + 1
}
if index != chars.length() {
raise InvalidCobertura(
input_line, "text follows XML self-closing slash",
)
}
break
}
if closing {
raise InvalidCobertura(input_line, "closing XML tag has attributes")
}
let attribute_start = index
while index < chars.length() &&
!is_xml_space(chars[index]) &&
chars[index] != '=' &&
chars[index] != '/' {
index = index + 1
}
if attribute_start == index {
raise InvalidCobertura(input_line, "invalid XML attribute name")
}
let attribute_name = chars_to_string(chars, attribute_start, index)
while index < chars.length() && is_xml_space(chars[index]) {
index = index + 1
}
if index == chars.length() || chars[index] != '=' {
raise InvalidCobertura(
input_line,
"attribute \{attribute_name} is missing =",
)
}
index = index + 1
while index < chars.length() && is_xml_space(chars[index]) {
index = index + 1
}
if index == chars.length() || (chars[index] != '"' && chars[index] != '\'') {
raise InvalidCobertura(
input_line,
"attribute \{attribute_name} must have a quoted value",
)
}
let quote = chars[index]
index = index + 1
let value_start = index
while index < chars.length() && chars[index] != quote {
index = index + 1
}
if index == chars.length() {
raise InvalidCobertura(
input_line,
"unterminated value for attribute \{attribute_name}",
)
}
let raw_value = chars_to_string(chars, value_start, index)
attributes[attribute_name] = decode_xml_entities(raw_value, input_line)
index = index + 1
}
{ name, closing, self_closing, attributes, input_line }
}
///|
fn scan_xml_tags(input : String) -> Array[XmlTag] raise CoverageError {
let scanner : XmlScanner = { chars: input.to_array(), index: 0, line: 1 }
let tags : Array[XmlTag] = []
while scanner.index < scanner.chars.length() {
if scanner.chars[scanner.index] != '<' {
scanner.advance(1)
continue
}
let start_line = scanner.line
if chars_have_prefix(scanner.chars, scanner.index, "", start_line)
continue
}
if chars_have_prefix(scanner.chars, scanner.index, "") {
scanner.advance(2)
scanner.skip_until("?>", start_line)
continue
}
if chars_have_prefix(scanner.chars, scanner.index, "", start_line)
continue
}
scanner.advance(1)
if scanner.index < scanner.chars.length() &&
scanner.chars[scanner.index] == '!' {
scanner.skip_until(">", start_line)
continue
}
let content_start = scanner.index
let mut quote : Char? = None
while scanner.index < scanner.chars.length() {
let char = scanner.chars[scanner.index]
match quote {
Some(expected) => if char == expected { quote = None }
None =>
if char == '"' || char == '\'' {
quote = Some(char)
} else if char == '>' {
break
}
}
scanner.advance(1)
}
if scanner.index == scanner.chars.length() {
raise InvalidCobertura(start_line, "unterminated XML tag")
}
let content = chars_to_string(scanner.chars, content_start, scanner.index)
scanner.advance(1)
tags.push(parse_xml_tag(content, start_line))
}
tags
}
///|
fn xml_escape_attribute(input : String) -> String {
input
.replace_all(old="&", new="&")
.replace_all(old="<", new="<")
.replace_all(old=">", new=">")
.replace_all(old="\"", new=""")
.replace_all(old="'", new="'")
}