///|
pub fn FormatOptions::default() -> FormatOptions {
{ missing_argument: RaiseError, maximum_depth: 100 }
}
///|
pub fn FormatOptions::permissive() -> FormatOptions {
{ missing_argument: KeepPlaceholder, maximum_depth: 100 }
}
///|
pub fn FormatOptions::missing_argument(
self : FormatOptions,
) -> MissingArgumentBehavior {
self.missing_argument
}
///|
pub fn FormatOptions::maximum_depth(self : FormatOptions) -> Int {
self.maximum_depth
}
///|
pub fn MissingArgumentBehavior::name(self : MissingArgumentBehavior) -> String {
match self {
RaiseError => "error"
KeepPlaceholder => "placeholder"
ReplaceWithEmpty => "empty"
}
}
///|
pub fn Argument::kind(self : Argument) -> String {
match self {
Text(_) => "text"
Number(_) => "number"
}
}
///|
pub fn Argument::as_text(self : Argument) -> String {
match self {
Text(value) => value
Number(value) => value.to_string()
}
}
///|
pub fn Argument::as_number(self : Argument) -> Int raise MessageError {
match self {
Number(value) => value
Text(_) => raise WrongArgumentType("value")
}
}
///|
pub fn Argument::is_text(self : Argument) -> Bool {
self is Text(_)
}
///|
pub fn Argument::is_number(self : Argument) -> Bool {
self is Number(_)
}
///|
fn configurable_argument_text(
arguments : Map[String, Argument],
name : String,
behavior : MissingArgumentBehavior,
) -> String raise MessageError {
match arguments.get(name) {
Some(value) => value.as_text()
None =>
match behavior {
RaiseError => raise MissingArgument(name)
KeepPlaceholder => "{" + name + "}"
ReplaceWithEmpty => ""
}
}
}
///|
fn required_argument_number(
arguments : Map[String, Argument],
name : String,
) -> Int raise MessageError {
match arguments.get(name) {
Some(Number(value)) => value
Some(Text(_)) => raise WrongArgumentType(name)
None => raise MissingArgument(name)
}
}
///|
fn format_event(
kind : FormatEventKind,
depth : Int,
path : String,
output : String,
argument? : String,
selector? : String,
) -> FormatEvent {
{ kind, depth, path, argument, selector, output }
}
///|
fn child_path(path : String, index : Int) -> String {
if path == "" {
index.to_string()
} else {
path + "/" + index.to_string()
}
}
///|
fn selectors_for_cases(
cases : Map[String, Array[MessageNode]],
) -> Array[String] {
let selectors : Array[String] = []
for selector, _ in cases {
selectors.push(selector)
}
selectors.sort()
selectors
}
///|
fn select_text_branch(
arguments : Map[String, Argument],
name : String,
cases : Map[String, Array[MessageNode]],
) -> String raise MessageError {
let selector = match arguments.get(name) {
Some(value) => value.as_text()
None => raise MissingArgument(name)
}
if cases.contains(selector) {
selector
} else if cases.contains("other") {
"other"
} else {
raise MissingChoice(selector)
}
}
///|
fn select_numeric_branch(
arguments : Map[String, Argument],
name : String,
kind : String,
cases : Map[String, Array[MessageNode]],
locale : String,
) -> (String, Int) raise MessageError {
let value = required_argument_number(arguments, name)
let selection = select_plural_branch(
locale,
value,
kind == "selectordinal",
selectors_for_cases(cases),
)
(selection.selected_selector, value)
}
///|
fn evaluate_nodes_with_trace(
nodes : Array[MessageNode],
arguments : Map[String, Argument],
locale : String,
pound : Int?,
depth : Int,
path : String,
options : FormatOptions,
events : Array[FormatEvent],
) -> String raise MessageError {
if depth > options.maximum_depth {
raise EvaluationLimit(options.maximum_depth)
}
let output = StringBuilder::new()
for index, node in nodes {
let node_path = child_path(path, index)
match node {
TextNode(text) => {
output.write_string(text)
events.push(format_event(EmitText, depth, node_path, text))
}
ArgumentNode(name) => {
let value = configurable_argument_text(
arguments,
name,
options.missing_argument,
)
output.write_string(value)
events.push(
format_event(EmitArgument, depth, node_path, value, argument=name),
)
}
PoundNode => {
let value = match pound {
Some(number) => number.to_string()
None => "#"
}
output.write_string(value)
events.push(format_event(EmitPound, depth, node_path, value))
}
ChoiceNode(name, kind, cases) => {
let (selector, nested_pound) = if kind == "select" {
(select_text_branch(arguments, name, cases), pound)
} else {
let (selected, number) = select_numeric_branch(
arguments, name, kind, cases, locale,
)
(selected, Some(number))
}
events.push(
format_event(
SelectBranch,
depth,
node_path,
"",
argument=name,
selector~,
),
)
let branch = match cases.get(selector) {
Some(value) => value
None => raise MissingChoice(selector)
}
output.write_string(
evaluate_nodes_with_trace(
branch,
arguments,
locale,
nested_pound,
depth + 1,
node_path + ":" + selector,
options,
events,
),
)
}
}
}
output.to_string()
}
///|
/// Format a message and retain a deterministic evaluation trace.
pub fn format_with_trace(
message : Message,
arguments : Map[String, Argument],
locale : String,
options? : FormatOptions = FormatOptions::default(),
) -> FormattedMessage raise MessageError {
let events : Array[FormatEvent] = []
let value = evaluate_nodes_with_trace(
message.nodes,
arguments,
locale,
None,
1,
"",
options,
events,
)
{ value, events }
}
///|
/// Format a message using explicit runtime options.
pub fn format_with_options(
message : Message,
arguments : Map[String, Argument],
locale : String,
options : FormatOptions,
) -> String raise MessageError {
format_with_trace(message, arguments, locale, options~).value
}
///|
pub fn FormatEventKind::name(self : FormatEventKind) -> String {
match self {
EmitText => "text"
EmitArgument => "argument"
EmitPound => "pound"
SelectBranch => "choice"
}
}
///|
pub fn FormatEvent::kind(self : FormatEvent) -> FormatEventKind {
self.kind
}
///|
pub fn FormatEvent::depth(self : FormatEvent) -> Int {
self.depth
}
///|
pub fn FormatEvent::path(self : FormatEvent) -> String {
self.path
}
///|
pub fn FormatEvent::argument(self : FormatEvent) -> String? {
self.argument
}
///|
pub fn FormatEvent::selector(self : FormatEvent) -> String? {
self.selector
}
///|
pub fn FormatEvent::output(self : FormatEvent) -> String {
self.output
}
///|
pub fn FormatEvent::is_choice(self : FormatEvent) -> Bool {
self.kind == SelectBranch
}
///|
pub fn FormatEvent::emits_output(self : FormatEvent) -> Bool {
self.kind != SelectBranch
}
///|
pub fn FormatEvent::has_argument(self : FormatEvent) -> Bool {
self.argument is Some(_)
}
///|
pub fn FormatEvent::has_selector(self : FormatEvent) -> Bool {
self.selector is Some(_)
}
///|
pub fn FormatEvent::display(self : FormatEvent) -> String {
let detail = match (self.argument, self.selector) {
(Some(argument), Some(selector)) => " \{argument}=\{selector}"
(Some(argument), None) => " \{argument}"
(None, Some(selector)) => " \{selector}"
(None, None) => ""
}
"\{self.path} \{self.kind.name()}\{detail} -> \{self.output}"
}
///|
pub fn FormattedMessage::value(self : FormattedMessage) -> String {
self.value
}
///|
pub fn FormattedMessage::events(self : FormattedMessage) -> Array[FormatEvent] {
self.events
}
///|
pub fn FormattedMessage::event_count(self : FormattedMessage) -> Int {
self.events.length()
}
///|
pub fn FormattedMessage::maximum_depth(self : FormattedMessage) -> Int {
let mut maximum = 0
for event in self.events {
if event.depth > maximum {
maximum = event.depth
}
}
maximum
}
///|
pub fn FormattedMessage::events_for_argument(
self : FormattedMessage,
argument : String,
) -> Array[FormatEvent] {
let output : Array[FormatEvent] = []
for event in self.events {
if event.argument is Some(name) && name == argument {
output.push(event)
}
}
output
}
///|
pub fn FormattedMessage::choice_events(
self : FormattedMessage,
) -> Array[FormatEvent] {
let output : Array[FormatEvent] = []
for event in self.events {
if event.kind == SelectBranch {
output.push(event)
}
}
output
}
///|
pub fn FormattedMessage::trace_text(self : FormattedMessage) -> String {
let output = StringBuilder::new()
for index, event in self.events {
if index > 0 {
output.write_char('\n')
}
output.write_string(event.display())
}
output.to_string()
}