///|
/// Describes an invalid message template and its character offset.
pub enum MessageParseError {
UnexpectedEnd(Int, String)
UnexpectedCharacter(Int, Char)
ExpectedToken(Int, String)
EmptyArgument(Int)
UnknownArgumentType(Int, String)
MissingOtherCase(Int)
DuplicateCase(Int, String)
InvalidOffset(Int, String)
TrailingContent(Int)
} derive(Eq, Debug)
///|
pub fn MessageParseError::position(self : MessageParseError) -> Int {
match self {
UnexpectedEnd(position, _)
| UnexpectedCharacter(position, _)
| ExpectedToken(position, _)
| EmptyArgument(position)
| UnknownArgumentType(position, _)
| MissingOtherCase(position)
| DuplicateCase(position, _)
| InvalidOffset(position, _)
| TrailingContent(position) => position
}
}
///|
pub fn MessageParseError::message(self : MessageParseError) -> String {
match self {
UnexpectedEnd(position, expected) =>
"unexpected end at \{position}; expected \{expected}"
UnexpectedCharacter(position, character) =>
"unexpected character '\{character}' at \{position}"
ExpectedToken(position, token) => "expected \{token} at \{position}"
EmptyArgument(position) => "empty argument at \{position}"
UnknownArgumentType(position, argument_type) =>
"unknown argument type '\{argument_type}' at \{position}"
MissingOtherCase(position) =>
"select or plural argument at \{position} requires an other case"
DuplicateCase(position, selector) =>
"duplicate case '\{selector}' at \{position}"
InvalidOffset(position, value) =>
"invalid plural offset '\{value}' at \{position}"
TrailingContent(position) => "unexpected trailing content at \{position}"
}
}
///|
priv struct MessageParser {
chars : Array[Char]
mut index : Int
}
///|
fn MessageParser::new(source : String) -> MessageParser {
let chars : Array[Char] = []
for character in source {
chars.push(character)
}
{ chars, index: 0 }
}
///|
fn MessageParser::at_end(self : MessageParser) -> Bool {
self.index >= self.chars.length()
}
///|
fn MessageParser::peek(self : MessageParser) -> Char? {
if self.at_end() {
None
} else {
Some(self.chars[self.index])
}
}
///|
fn MessageParser::advance(self : MessageParser) -> Char? {
if self.at_end() {
None
} else {
let value = self.chars[self.index]
self.index = self.index + 1
Some(value)
}
}
///|
fn MessageParser::skip_spaces(self : MessageParser) -> Unit {
while !self.at_end() && self.chars[self.index].is_ascii_whitespace() {
self.index = self.index + 1
}
}
///|
fn message_is_name_char(character : Char) -> Bool {
character.is_ascii_alphabetic() ||
character.is_ascii_digit() ||
character == '_' ||
character == '-' ||
character == '.'
}
///|
fn chars_to_string(chars : Array[Char]) -> String {
let output = StringBuilder::new()
for character in chars {
output.write_char(character)
}
output.to_string()
}
///|
fn MessageParser::read_name(self : MessageParser) -> String {
self.skip_spaces()
let output : Array[Char] = []
while !self.at_end() && message_is_name_char(self.chars[self.index]) {
output.push(self.chars[self.index])
self.index = self.index + 1
}
chars_to_string(output)
}
///|
fn MessageParser::expect(
self : MessageParser,
expected : Char,
) -> Result[Unit, MessageParseError] {
self.skip_spaces()
match self.advance() {
Some(actual) =>
if actual == expected {
Ok(())
} else {
Err(UnexpectedCharacter(self.index - 1, actual))
}
None => Err(UnexpectedEnd(self.index, "'\{expected}'"))
}
}
///|
fn append_text_node(nodes : Array[MessageNode], text : StringBuilder) -> Unit {
let value = text.to_string()
if value.length() > 0 {
nodes.push(Text(value))
}
}
///|
fn MessageParser::parse_quoted_text(
self : MessageParser,
text : StringBuilder,
) -> Result[Unit, MessageParseError] {
let quote_position = self.index
self.index = self.index + 1
if !self.at_end() && self.chars[self.index] == '\'' {
text.write_char('\'')
self.index = self.index + 1
return Ok(())
}
while !self.at_end() {
let character = self.chars[self.index]
self.index = self.index + 1
if character == '\'' {
if !self.at_end() && self.chars[self.index] == '\'' {
text.write_char('\'')
self.index = self.index + 1
} else {
return Ok(())
}
} else {
text.write_char(character)
}
}
Err(UnexpectedEnd(quote_position, "closing apostrophe"))
}
///|
fn MessageParser::parse_sequence(
self : MessageParser,
nested : Bool,
) -> Result[Array[MessageNode], MessageParseError] {
let nodes : Array[MessageNode] = []
let mut text = StringBuilder::new()
while !self.at_end() {
let character = self.chars[self.index]
if character == '{' {
append_text_node(nodes, text)
text = StringBuilder::new()
match self.parse_argument() {
Ok(node) => nodes.push(node)
Err(error) => return Err(error)
}
} else if character == '}' {
if nested {
append_text_node(nodes, text)
return Ok(nodes)
}
return Err(UnexpectedCharacter(self.index, character))
} else if character == '\'' {
match self.parse_quoted_text(text) {
Ok(_) => ()
Err(error) => return Err(error)
}
} else {
text.write_char(character)
self.index = self.index + 1
}
}
if nested {
Err(UnexpectedEnd(self.index, "'}'"))
} else {
append_text_node(nodes, text)
Ok(nodes)
}
}
///|
fn MessageParser::parse_argument(
self : MessageParser,
) -> Result[MessageNode, MessageParseError] {
let opening_position = self.index
self.index = self.index + 1
let name = self.read_name()
if name.length() == 0 {
return Err(EmptyArgument(opening_position))
}
self.skip_spaces()
match self.peek() {
Some('}') => {
self.index = self.index + 1
Ok(Argument(name))
}
Some(',') => {
self.index = self.index + 1
let type_position = self.index
let argument_type = self.read_name()
if argument_type.length() == 0 {
return Err(ExpectedToken(type_position, "argument type"))
}
match argument_type {
"select" => self.parse_select_argument(name, opening_position)
"plural" => self.parse_plural_argument(name, Cardinal, opening_position)
"selectordinal" =>
self.parse_plural_argument(name, Ordinal, opening_position)
_ => Err(UnknownArgumentType(type_position, argument_type))
}
}
Some(character) => Err(UnexpectedCharacter(self.index, character))
None => Err(UnexpectedEnd(self.index, "',' or '}'"))
}
}
///|
fn MessageParser::parse_select_argument(
self : MessageParser,
name : String,
opening_position : Int,
) -> Result[MessageNode, MessageParseError] {
match self.expect(',') {
Err(error) => return Err(error)
Ok(_) => ()
}
match self.parse_cases(false, false) {
Ok((_, cases)) => {
match self.expect('}') {
Err(error) => return Err(error)
Ok(_) => ()
}
if !has_message_case(cases, "other") {
return Err(MissingOtherCase(opening_position))
}
Ok(Select(name, cases))
}
Err(error) => Err(error)
}
}
///|
fn MessageParser::parse_plural_argument(
self : MessageParser,
name : String,
kind : PluralKind,
opening_position : Int,
) -> Result[MessageNode, MessageParseError] {
match self.expect(',') {
Err(error) => return Err(error)
Ok(_) => ()
}
match self.parse_cases(true, true) {
Ok((offset, cases)) => {
match self.expect('}') {
Err(error) => return Err(error)
Ok(_) => ()
}
if !has_message_case(cases, "other") {
return Err(MissingOtherCase(opening_position))
}
Ok(Plural(name, offset, kind, cases))
}
Err(error) => Err(error)
}
}
///|
fn MessageParser::read_selector(
self : MessageParser,
allow_exact : Bool,
) -> String {
self.skip_spaces()
let output : Array[Char] = []
if allow_exact && self.peek() == Some('=') {
output.push('=')
self.index = self.index + 1
}
while !self.at_end() && message_is_name_char(self.chars[self.index]) {
output.push(self.chars[self.index])
self.index = self.index + 1
}
chars_to_string(output)
}
///|
fn MessageParser::read_nonnegative_int(
self : MessageParser,
) -> Result[Int, MessageParseError] {
let start = self.index
let mut value = 0
let mut found = false
while !self.at_end() && self.chars[self.index].is_ascii_digit() {
found = true
value = value * 10 + self.chars[self.index].to_int() - '0'.to_int()
self.index = self.index + 1
}
if found {
Ok(value)
} else {
Err(InvalidOffset(start, ""))
}
}
///|
fn MessageParser::try_read_offset(
self : MessageParser,
) -> Result[Int, MessageParseError] {
self.skip_spaces()
let start = self.index
let word = self.read_name()
if word != "offset" {
self.index = start
return Ok(0)
}
match self.expect(':') {
Err(error) => return Err(error)
Ok(_) => ()
}
self.skip_spaces()
self.read_nonnegative_int()
}
///|
fn MessageParser::parse_cases(
self : MessageParser,
allow_exact : Bool,
allow_offset : Bool,
) -> Result[(Int, Array[MessageCase]), MessageParseError] {
let mut offset = 0
if allow_offset {
match self.try_read_offset() {
Ok(value) => offset = value
Err(error) => return Err(error)
}
}
let cases : Array[MessageCase] = []
while true {
self.skip_spaces()
match self.peek() {
Some('}') => break
None => return Err(UnexpectedEnd(self.index, "case or '}'"))
_ => ()
}
let selector_position = self.index
let selector = self.read_selector(allow_exact)
if selector.length() == 0 {
return Err(ExpectedToken(selector_position, "case selector"))
}
if has_message_case(cases, selector) {
return Err(DuplicateCase(selector_position, selector))
}
match self.expect('{') {
Err(error) => return Err(error)
Ok(_) => ()
}
match self.parse_sequence(true) {
Err(error) => return Err(error)
Ok(nodes) => {
match self.expect('}') {
Err(error) => return Err(error)
Ok(_) => ()
}
cases.push({ selector, nodes })
}
}
}
Ok((offset, cases))
}
///|
fn has_message_case(cases : Array[MessageCase], selector : String) -> Bool {
for branch in cases {
if branch.selector == selector {
return true
}
}
false
}
///|
/// Parses an ICU-inspired message template.
///
/// Supported arguments are simple variables, `select`, `plural`, and
/// `selectordinal`. Plural branches accept exact selectors such as `=0` and
/// optional non-negative offsets. Apostrophes quote syntax characters.
pub fn MessageTemplate::parse(
source : String,
) -> Result[MessageTemplate, MessageParseError] {
let parser = MessageParser::new(source)
match parser.parse_sequence(false) {
Ok(nodes) =>
if parser.at_end() {
Ok({ source, nodes })
} else {
Err(TrailingContent(parser.index))
}
Err(error) => Err(error)
}
}