///|
fn clamp_offset(source : String, offset : Int) -> Int {
if offset < 0 {
0
} else if offset > source.length() {
source.length()
} else {
offset
}
}
///|
/// Convert a zero-based character offset into a one-based line and column.
pub fn source_position(source : String, offset : Int) -> SourcePosition {
let target = clamp_offset(source, offset)
let mut current = 0
let mut line = 1
let mut column = 1
for ch in source {
if current >= target {
break
}
if ch == '\n' {
line += 1
column = 1
} else {
column += 1
}
current += 1
}
{ offset: target, line, column }
}
///|
/// Create a half-open source span from two offsets.
pub fn source_span(source : String, start : Int, end : Int) -> SourceSpan {
let safe_start = clamp_offset(source, start)
let safe_end = if end < safe_start {
safe_start
} else {
clamp_offset(source, end)
}
{
start: source_position(source, safe_start),
end: source_position(source, safe_end),
}
}
///|
pub fn SourcePosition::offset(self : SourcePosition) -> Int {
self.offset
}
///|
pub fn SourcePosition::line(self : SourcePosition) -> Int {
self.line
}
///|
pub fn SourcePosition::column(self : SourcePosition) -> Int {
self.column
}
///|
pub fn SourcePosition::display(self : SourcePosition) -> String {
"\{self.line}:\{self.column}"
}
///|
pub fn SourceSpan::start(self : SourceSpan) -> SourcePosition {
self.start
}
///|
pub fn SourceSpan::end(self : SourceSpan) -> SourcePosition {
self.end
}
///|
pub fn SourceSpan::length(self : SourceSpan) -> Int {
self.end.offset - self.start.offset
}
///|
pub fn MessageDiagnostic::code(self : MessageDiagnostic) -> String {
self.code
}
///|
pub fn MessageDiagnostic::message(self : MessageDiagnostic) -> String {
self.message
}
///|
pub fn MessageDiagnostic::position(self : MessageDiagnostic) -> SourcePosition {
self.position
}
///|
pub fn MessageDiagnostic::display(self : MessageDiagnostic) -> String {
"\{self.code} at \{self.position.display()}: \{self.message}"
}
///|
fn parse_error_code(message : String) -> String {
if message.has_prefix("Expected argument name") {
"expected-argument"
} else if message.has_prefix("Unsupported formatter") {
"unsupported-formatter"
} else if message.has_prefix("Did you mean 'selectordinal'") {
"invalid-selectordinal-name"
} else if message.has_prefix("Duplicate selector") {
"duplicate-selector"
} else if message.has_prefix("Invalid plural selector") {
"invalid-plural-selector"
} else if message.has_prefix("Choice needs an other") {
"missing-other"
} else if message.has_prefix("Unclosed") {
"unclosed-branch"
} else if message.has_prefix("Dangling escape") {
"dangling-escape"
} else if message.has_prefix("Expected choice selector") {
"expected-selector"
} else {
"message-syntax"
}
}
///|
/// Convert a parser error into a stable diagnostic with line and column.
pub fn diagnostic_from_error(
source : String,
error : MessageError,
) -> MessageDiagnostic {
match error {
ParseError(position~, message~) =>
{
code: parse_error_code(message),
message,
position: source_position(source, position),
}
InvalidCatalog(message) =>
{ code: "invalid-catalog", message, position: source_position(source, 0) }
InvalidLocale(message) =>
{ code: "invalid-locale", message, position: source_position(source, 0) }
InvalidCommand(message) =>
{ code: "invalid-command", message, position: source_position(source, 0) }
EvaluationLimit(limit) =>
{
code: "evaluation-limit",
message: "Message nesting exceeds limit \{limit}.",
position: source_position(source, 0),
}
MissingArgument(name) =>
{
code: "missing-argument",
message: "Missing argument '\{name}'.",
position: source_position(source, 0),
}
WrongArgumentType(name) =>
{
code: "wrong-argument-type",
message: "Argument '\{name}' has the wrong type.",
position: source_position(source, 0),
}
MissingChoice(selector) =>
{
code: "missing-choice",
message: "No branch matches '\{selector}'.",
position: source_position(source, 0),
}
}
}
///|
/// Parse a message and return a diagnostic instead of raising on failure.
pub fn diagnose_message(template : String) -> MessageDiagnostic? {
try parse_message(template) catch {
error => Some(diagnostic_from_error(template, error))
} noraise {
_ => None
}
}
///|
pub fn ChoiceKind::name(self : ChoiceKind) -> String {
match self {
Select => "select"
Plural => "plural"
SelectOrdinal => "selectordinal"
}
}
///|
pub fn ChoiceKind::is_numeric(self : ChoiceKind) -> Bool {
match self {
Select => false
Plural | SelectOrdinal => true
}
}
///|
pub fn ChoiceKind::from_name(name : String) -> ChoiceKind? {
match name {
"select" => Some(Select)
"plural" => Some(Plural)
"selectordinal" => Some(SelectOrdinal)
_ => None
}
}
///|
pub fn ArgumentRole::name(self : ArgumentRole) -> String {
match self {
Interpolation => "interpolation"
SelectSelector => "select"
CardinalSelector => "plural"
OrdinalSelector => "selectordinal"
}
}
///|
pub fn ArgumentRole::expects_number(self : ArgumentRole) -> Bool {
match self {
CardinalSelector | OrdinalSelector => true
Interpolation | SelectSelector => false
}
}
///|
priv struct ArgumentAccumulator {
roles : Map[String, ArgumentRole]
mut occurrences : Int
}
///|
fn ArgumentAccumulator::new() -> ArgumentAccumulator {
{ roles: Map([]), occurrences: 0 }
}
///|
fn ArgumentAccumulator::record(
self : ArgumentAccumulator,
role : ArgumentRole,
) -> Unit {
self.roles[role.name()] = role
self.occurrences += 1
}
///|
fn record_argument(
uses : Map[String, ArgumentAccumulator],
name : String,
role : ArgumentRole,
) -> Unit {
let accumulator = match uses.get(name) {
Some(value) => value
None => {
let value = ArgumentAccumulator::new()
uses[name] = value
value
}
}
accumulator.record(role)
}
///|
priv struct AnalysisAccumulator {
arguments : Map[String, ArgumentAccumulator]
mut node_count : Int
mut text_node_count : Int
mut argument_node_count : Int
mut choice_node_count : Int
mut pound_node_count : Int
mut maximum_depth : Int
}
///|
fn AnalysisAccumulator::new() -> AnalysisAccumulator {
{
arguments: Map([]),
node_count: 0,
text_node_count: 0,
argument_node_count: 0,
choice_node_count: 0,
pound_node_count: 0,
maximum_depth: 0,
}
}
///|
fn choice_role(kind : String) -> ArgumentRole {
match kind {
"select" => SelectSelector
"selectordinal" => OrdinalSelector
_ => CardinalSelector
}
}
///|
fn analyze_nodes(
nodes : Array[MessageNode],
depth : Int,
accumulator : AnalysisAccumulator,
) -> Unit {
if depth > accumulator.maximum_depth {
accumulator.maximum_depth = depth
}
for node in nodes {
accumulator.node_count += 1
match node {
TextNode(_) => accumulator.text_node_count += 1
ArgumentNode(name) => {
accumulator.argument_node_count += 1
record_argument(accumulator.arguments, name, Interpolation)
}
PoundNode => accumulator.pound_node_count += 1
ChoiceNode(name, kind, cases) => {
accumulator.choice_node_count += 1
record_argument(accumulator.arguments, name, choice_role(kind))
for _, branch in cases {
analyze_nodes(branch, depth + 1, accumulator)
}
}
}
}
}
///|
fn sorted_argument_names(
arguments : Map[String, ArgumentAccumulator],
) -> Array[String] {
let names : Array[String] = []
for name, _ in arguments {
names.push(name)
}
names.sort()
names
}
///|
fn accumulator_roles(accumulator : ArgumentAccumulator) -> Array[ArgumentRole] {
let roles : Array[ArgumentRole] = []
for _, role in accumulator.roles {
roles.push(role)
}
roles.sort_by((left, right) => left.name().compare(right.name()))
roles
}
///|
/// Analyze the argument signature and structural complexity of a message.
pub fn Message::analyze(self : Message) -> MessageAnalysis {
let accumulator = AnalysisAccumulator::new()
analyze_nodes(self.nodes, 1, accumulator)
let arguments : Array[ArgumentUse] = []
for name in sorted_argument_names(accumulator.arguments) {
let argument_use = accumulator.arguments[name]
arguments.push({
name,
roles: accumulator_roles(argument_use),
occurrences: argument_use.occurrences,
})
}
{
arguments,
node_count: accumulator.node_count,
text_node_count: accumulator.text_node_count,
argument_node_count: accumulator.argument_node_count,
choice_node_count: accumulator.choice_node_count,
pound_node_count: accumulator.pound_node_count,
maximum_depth: accumulator.maximum_depth,
}
}
///|
pub fn Message::argument_uses(self : Message) -> Array[ArgumentUse] {
self.analyze().arguments
}
///|
pub fn Message::node_count(self : Message) -> Int {
self.analyze().node_count
}
///|
pub fn Message::maximum_depth(self : Message) -> Int {
self.analyze().maximum_depth
}
///|
pub fn ArgumentUse::name(self : ArgumentUse) -> String {
self.name
}
///|
pub fn ArgumentUse::roles(self : ArgumentUse) -> Array[ArgumentRole] {
self.roles
}
///|
pub fn ArgumentUse::occurrences(self : ArgumentUse) -> Int {
self.occurrences
}
///|
pub fn ArgumentUse::uses_role(self : ArgumentUse, role : ArgumentRole) -> Bool {
for existing in self.roles {
if existing == role {
return true
}
}
false
}
///|
pub fn ArgumentUse::expects_number(self : ArgumentUse) -> Bool {
for role in self.roles {
if role.expects_number() {
return true
}
}
false
}
///|
pub fn MessageAnalysis::arguments(self : MessageAnalysis) -> Array[ArgumentUse] {
self.arguments
}
///|
pub fn MessageAnalysis::node_count(self : MessageAnalysis) -> Int {
self.node_count
}
///|
pub fn MessageAnalysis::text_node_count(self : MessageAnalysis) -> Int {
self.text_node_count
}
///|
pub fn MessageAnalysis::argument_node_count(self : MessageAnalysis) -> Int {
self.argument_node_count
}
///|
pub fn MessageAnalysis::choice_node_count(self : MessageAnalysis) -> Int {
self.choice_node_count
}
///|
pub fn MessageAnalysis::pound_node_count(self : MessageAnalysis) -> Int {
self.pound_node_count
}
///|
pub fn MessageAnalysis::maximum_depth(self : MessageAnalysis) -> Int {
self.maximum_depth
}