///|
priv struct ChoiceCollector {
choices : Array[ChoiceUse]
mut ordinal : Int
}
///|
fn ChoiceCollector::new() -> ChoiceCollector {
{ choices: [], ordinal: 0 }
}
///|
fn sorted_selectors(cases : Map[String, Array[MessageNode]]) -> Array[String] {
let selectors : Array[String] = []
for selector, _ in cases {
selectors.push(selector)
}
selectors.sort()
selectors
}
///|
fn collect_choice_uses(
nodes : Array[MessageNode],
depth : Int,
collector : ChoiceCollector,
) -> Unit {
for node in nodes {
match node {
ChoiceNode(argument, kind_name, cases) => {
let kind = match ChoiceKind::from_name(kind_name) {
Some(value) => value
None => Select
}
let ordinal = collector.ordinal
collector.ordinal += 1
collector.choices.push({
argument,
kind,
selectors: sorted_selectors(cases),
depth,
ordinal,
})
for _, branch in cases {
collect_choice_uses(branch, depth + 1, collector)
}
}
TextNode(_) | ArgumentNode(_) | PoundNode => ()
}
}
}
///|
/// Return a semantic signature for linting and tooling.
pub fn Message::signature(self : Message) -> MessageSignature {
let collector = ChoiceCollector::new()
collect_choice_uses(self.nodes, 1, collector)
{ arguments: self.argument_uses(), choices: collector.choices }
}
///|
pub fn Message::choice_uses(self : Message) -> Array[ChoiceUse] {
self.signature().choices
}
///|
pub fn Message::nodes(self : Message) -> Array[MessageNode] {
self.nodes
}
///|
/// Construct a message from AST nodes.
pub fn Message::from_nodes(nodes : Array[MessageNode]) -> Message {
{ nodes, }
}
///|
pub fn ChoiceUse::argument(self : ChoiceUse) -> String {
self.argument
}
///|
pub fn ChoiceUse::kind(self : ChoiceUse) -> ChoiceKind {
self.kind
}
///|
pub fn ChoiceUse::selectors(self : ChoiceUse) -> Array[String] {
self.selectors
}
///|
pub fn ChoiceUse::depth(self : ChoiceUse) -> Int {
self.depth
}
///|
pub fn ChoiceUse::ordinal(self : ChoiceUse) -> Int {
self.ordinal
}
///|
pub fn ChoiceUse::has_selector(self : ChoiceUse, selector : String) -> Bool {
for existing in self.selectors {
if existing == selector {
return true
}
}
false
}
///|
pub fn ChoiceUse::exact_selectors(self : ChoiceUse) -> Array[String] {
let output : Array[String] = []
for selector in self.selectors {
if selector.has_prefix("=") {
output.push(selector)
}
}
output
}
///|
pub fn ChoiceUse::category_selectors(self : ChoiceUse) -> Array[String] {
let output : Array[String] = []
for selector in self.selectors {
if !selector.has_prefix("=") {
output.push(selector)
}
}
output
}
///|
pub fn ChoiceUse::identifier(self : ChoiceUse) -> String {
"\{self.argument}:\{self.kind.name()}:\{self.ordinal}"
}
///|
pub fn MessageSignature::arguments(
self : MessageSignature,
) -> Array[ArgumentUse] {
self.arguments
}
///|
pub fn MessageSignature::choices(self : MessageSignature) -> Array[ChoiceUse] {
self.choices
}
///|
pub fn MessageSignature::argument(
self : MessageSignature,
name : String,
) -> ArgumentUse? {
for argument in self.arguments {
if argument.name == name {
return Some(argument)
}
}
None
}
///|
pub fn MessageSignature::has_argument(
self : MessageSignature,
name : String,
) -> Bool {
self.argument(name) is Some(_)
}
///|
pub fn MessageSignature::choices_for_argument(
self : MessageSignature,
name : String,
) -> Array[ChoiceUse] {
let output : Array[ChoiceUse] = []
for choice in self.choices {
if choice.argument == name {
output.push(choice)
}
}
output
}
///|
pub fn MessageSignature::choices_of_kind(
self : MessageSignature,
kind : ChoiceKind,
) -> Array[ChoiceUse] {
let output : Array[ChoiceUse] = []
for choice in self.choices {
if choice.kind == kind {
output.push(choice)
}
}
output
}
///|
pub fn MessageSignature::choice_count(self : MessageSignature) -> Int {
self.choices.length()
}
///|
pub fn MessageSignature::argument_count(self : MessageSignature) -> Int {
self.arguments.length()
}
///|
fn escape_text_node(text : String) -> String {
let output = StringBuilder::new()
for ch in text {
match ch {
'{' | '}' | '\\' | '#' => {
output.write_char('\\')
output.write_char(ch)
}
_ => output.write_char(ch)
}
}
output.to_string()
}
///|
fn render_nodes_as_template(nodes : Array[MessageNode]) -> String {
let output = StringBuilder::new()
for node in nodes {
match node {
TextNode(text) => output.write_string(escape_text_node(text))
ArgumentNode(name) => {
output.write_char('{')
output.write_string(name)
output.write_char('}')
}
PoundNode => output.write_char('#')
ChoiceNode(name, kind, cases) => {
output.write_char('{')
output.write_string(name)
output.write_string(", ")
output.write_string(kind)
output.write_string(", ")
let selectors = sorted_selectors(cases)
for index, selector in selectors {
if index > 0 {
output.write_char(' ')
}
output.write_string(selector)
output.write_string(" {")
output.write_string(render_nodes_as_template(cases[selector]))
output.write_char('}')
}
output.write_char('}')
}
}
}
output.to_string()
}
///|
/// Serialize an AST using MoonL10n's canonical spacing and escaping.
pub fn Message::to_template(self : Message) -> String {
render_nodes_as_template(self.nodes)
}
///|
fn append_indent(output : StringBuilder, level : Int) -> Unit {
for _ in 0.. Unit {
for node in nodes {
append_indent(output, depth)
match node {
TextNode(text) => {
output.write_string("text ")
output.write_string(text)
output.write_char('\n')
}
ArgumentNode(name) => {
output.write_string("argument ")
output.write_string(name)
output.write_char('\n')
}
PoundNode => output.write_string("pound\n")
ChoiceNode(name, kind, cases) => {
output.write_string(kind)
output.write_char(' ')
output.write_string(name)
output.write_char('\n')
for selector in sorted_selectors(cases) {
append_indent(output, depth + 1)
output.write_string("case ")
output.write_string(selector)
output.write_char('\n')
render_ast_nodes(output, cases[selector], depth + 2)
}
}
}
}
}
///|
/// Render a deterministic, human-readable AST tree.
pub fn Message::ast_text(self : Message) -> String {
let output = StringBuilder::new()
render_ast_nodes(output, self.nodes, 0)
output.to_string()
}
///|
fn role_names(roles : Array[ArgumentRole]) -> String {
let names : Array[String] = []
for role in roles {
names.push(role.name())
}
names.join(",")
}
///|
/// Render a compact signature suitable for CLI diagnostics.
pub fn MessageSignature::summary(self : MessageSignature) -> String {
let output = StringBuilder::new()
output.write_string("arguments=")
output.write_string(self.arguments.length().to_string())
output.write_string(" choices=")
output.write_string(self.choices.length().to_string())
for argument in self.arguments {
output.write_char('\n')
output.write_string(argument.name)
output.write_string(": ")
output.write_string(role_names(argument.roles))
output.write_string(" x")
output.write_string(argument.occurrences.to_string())
}
output.to_string()
}
///|
fn same_role_set(
left : Array[ArgumentRole],
right : Array[ArgumentRole],
) -> Bool {
if left.length() != right.length() {
return false
}
for role in left {
let mut found = false
for candidate in right {
if role == candidate {
found = true
break
}
}
if !found {
return false
}
}
true
}
///|
/// Compare argument names and roles while ignoring occurrence counts.
pub fn MessageSignature::compatible_with(
self : MessageSignature,
other : MessageSignature,
) -> Bool {
if self.arguments.length() != other.arguments.length() {
return false
}
for argument in self.arguments {
match other.argument(argument.name) {
Some(candidate) =>
if !same_role_set(argument.roles, candidate.roles) {
return false
}
None => return false
}
}
true
}
///|
/// Return sorted argument names used by a message.
pub fn MessageSignature::argument_names(
self : MessageSignature,
) -> Array[String] {
let names : Array[String] = []
for argument in self.arguments {
names.push(argument.name)
}
names.sort()
names
}