///|
fn error_message(code : String, args : Array[String?]) -> String {
let arg = if args.is_empty() { "None" } else { args[0].unwrap_or("None") }
match code {
"E0001" => "Generic error"
"E0002" => "Expected an entry start"
"E0003" => "Expected token: \"" + arg + "\""
"E0004" => "Expected a character from range: \"" + arg + "\""
"E0005" => "Expected message \"" + arg + "\" to have a value or attributes"
"E0006" => "Expected term \"-" + arg + "\" to have a value"
"E0007" => "Keyword cannot end with a whitespace"
"E0008" => "The callee has to be an upper-case identifier or a term"
"E0009" => "The argument name has to be a simple identifier"
"E0010" => "Expected one of the variants to be marked as default (*)"
"E0011" => "Expected at least one variant after \"->\""
"E0012" => "Expected value"
"E0013" => "Expected variant key"
"E0014" => "Expected literal"
"E0015" => "Only one variant can be marked as default (*)"
"E0016" => "Message references cannot be used as selectors"
"E0017" => "Terms cannot be used as selectors"
"E0018" => "Attributes of messages cannot be used as selectors"
"E0019" => "Attributes of terms cannot be used as placeables"
"E0020" => "Unterminated string expression"
"E0021" => "Positional arguments must not follow named arguments"
"E0022" => "Named arguments must be unique"
"E0024" => "Cannot access variants of a message."
"E0025" => "Unknown escape sequence: \\" + arg + "."
"E0026" => "Invalid Unicode escape sequence: " + arg + "."
"E0027" => "Unbalanced closing brace in TextElement."
"E0028" => "Expected an inline expression"
"E0029" => "Expected simple expression as selector"
"E9001" => "Syntax nesting exceeds configured depth limit"
_ => code
}
}
///|
fn Cursor::attribute(self : Cursor) -> Node raise ParseFailure {
let start = self.index
self.expect('.')
let id = self.identifier()
ignore(self.skip_blank_inline())
self.expect('=')
let value = match self.maybe_pattern() {
Some(v) => v
None => raise ParseFailure("E0012", [])
}
Node::with_span(Attribute(id, value), start, self.index)
}
///|
fn Cursor::attributes(self : Cursor) -> Array[Node] raise ParseFailure {
let attrs : Array[Node] = []
self.peek_blank()
while self.current_peek() == Some('.') {
self.skip_to_peek()
attrs.push(self.attribute())
self.peek_blank()
}
attrs
}
///|
fn Cursor::message(self : Cursor, term : Bool) -> Node raise ParseFailure {
let start = self.index
if term {
self.expect('-')
}
let id = self.identifier()
ignore(self.skip_blank_inline())
self.expect('=')
let value = self.maybe_pattern()
if term && value == None {
raise ParseFailure("E0006", [Some(identifier_name(id))])
}
let attrs = self.attributes()
let kind = if term {
Term(id, value.unwrap(), attrs, None)
} else {
if value == None && attrs.is_empty() {
raise ParseFailure("E0005", [Some(identifier_name(id))])
}
Message(id, value, attrs, None)
}
Node::with_span(kind, start, self.index)
}
///|
fn Cursor::entry(self : Cursor) -> Node raise ParseFailure {
match self.current() {
Some('#') => self.comment()
Some('-') => self.message(true)
ch if is_id_start(ch) => self.message(false)
_ => raise ParseFailure("E0002", [])
}
}
///|
fn Cursor::entry_or_junk(self : Cursor) -> Node {
let start = self.index
try {
let entry = self.entry()
self.expect_line_end()
entry
} catch {
ParseFailure(code, args) => {
let mut error_index = self.index
self.recover(start)
if self.index < error_index {
error_index = self.index
}
let annotation = Node::with_span(
Annotation(code, args, error_message(code, args)),
error_index,
error_index,
)
Node::with_span(
Junk(self.slice(start, self.index), [annotation]),
start,
self.index,
)
}
}
}
///|
fn attach_comment(entry : Node, comment : Node) -> Node {
let kind = match entry.kind {
Message(id, value, attrs, _) => Message(id, value, attrs, Some(comment))
Term(id, value, attrs, _) => Term(id, value, attrs, Some(comment))
_ => entry.kind
}
{ kind, span: { start: comment.span.start, end: entry.span.end, }, }
}
///|
/// Parse an FTL resource, preserving invalid entries as Junk with annotations.
/// Default nesting is limited to 64, returning recoverable E9001 on excess.
pub fn parse(source : String) -> Node {
let c = Cursor::new(source)
ignore(c.skip_blank_block())
let entries : Array[Node] = []
let mut pending : Node? = None
while c.current() != None {
let mut entry = c.entry_or_junk()
let blank_lines = c.skip_blank_block()
match entry.kind {
Comment(_) if blank_lines.is_empty() && c.current() != None => {
pending = Some(entry)
continue
}
_ => ()
}
if pending is Some(comment) {
match entry.kind {
Message(_, _, _, _) | Term(_, _, _, _) =>
entry = attach_comment(entry, comment)
_ => entries.push(comment)
}
pending = None
}
entries.push(entry)
}
Node::with_span(Resource(entries), 0, c.index)
}
///|
/// Parse the first entry, ignoring preceding valid comments as upstream does.
pub fn parse_entry(source : String) -> Node {
let c = Cursor::new(source)
ignore(c.skip_blank_block())
while c.current() == Some('#') {
let skipped = c.entry_or_junk()
match skipped.kind {
Junk(_, _) => return skipped
_ => ()
}
ignore(c.skip_blank_block())
}
c.entry_or_junk()
}