///|
/// Half-open Unicode scalar offsets, as used by Python fluent.syntax.
/// Offsets count both code points of CRLF even though parsing treats it as EOL.
pub(all) struct Span {
start : Int32
end : Int32
} derive(Eq)
///|
/// Typed Fluent syntax, deliberately independent of runtime evaluation.
pub(all) enum Syntax {
Resource(Array[Node])
Message(Node, Node?, Array[Node], Node?)
Term(Node, Node, Array[Node], Node?)
Pattern(Array[Node])
TextElement(String)
Placeable(Node)
StringLiteral(String)
NumberLiteral(String)
MessageReference(Node, Node?)
TermReference(Node, Node?, Node?)
VariableReference(Node)
FunctionReference(Node, Node)
SelectExpression(Node, Array[Node])
CallArguments(Array[Node], Array[Node])
Attribute(Node, Node)
Variant(Node, Node, Bool)
NamedArgument(Node, Node)
Identifier(String)
Comment(String)
GroupComment(String)
ResourceComment(String)
Junk(String, Array[Node])
Annotation(String, Array[String?], String)
} derive(Eq)
///|
pub(all) struct Node {
kind : Syntax
span : Span
} derive(Eq)
///|
/// Construct a synthetic node. Negative positions indicate no source origin.
pub fn Node::new(kind : Syntax) -> Node {
{ kind, span: { start: -1, end: -1, }, }
}
///|
pub fn Node::with_span(kind : Syntax, start : Int32, end : Int32) -> Node {
{ kind, span: { start, end, }, }
}
///|
pub fn Node::type_name(self : Node) -> String {
match self.kind {
Resource(_) => "Resource"
Message(_, _, _, _) => "Message"
Term(_, _, _, _) => "Term"
Pattern(_) => "Pattern"
TextElement(_) => "TextElement"
Placeable(_) => "Placeable"
StringLiteral(_) => "StringLiteral"
NumberLiteral(_) => "NumberLiteral"
MessageReference(_, _) => "MessageReference"
TermReference(_, _, _) => "TermReference"
VariableReference(_) => "VariableReference"
FunctionReference(_, _) => "FunctionReference"
SelectExpression(_, _) => "SelectExpression"
CallArguments(_, _) => "CallArguments"
Attribute(_, _) => "Attribute"
Variant(_, _, _) => "Variant"
NamedArgument(_, _) => "NamedArgument"
Identifier(_) => "Identifier"
Comment(_) => "Comment"
GroupComment(_) => "GroupComment"
ResourceComment(_) => "ResourceComment"
Junk(_, _) => "Junk"
Annotation(_, _, _) => "Annotation"
}
}
///|
fn json_nodes(nodes : Array[Node], spans : Bool) -> Json {
Json::array(nodes.map(n => n.to_json_ast(with_spans=spans)))
}
///|
fn json_optional(node : Node?, spans : Bool) -> Json {
match node {
Some(n) => n.to_json_ast(with_spans=spans)
None => Json::null()
}
}
///|
/// Mozilla-compatible AST JSON, suitable for differential tests and tooling.
/// Source offsets are omitted by default, not normalized to UTF-16 offsets.
pub fn Node::to_json_ast(self : Node, with_spans~ : Bool = false) -> Json {
let f : Map[String, Json] = { "type": Json::string(self.type_name()) }
match self.kind {
Resource(body) => f["body"] = json_nodes(body, with_spans)
Message(id, value, attrs, comment) => {
f["id"] = id.to_json_ast(with_spans~)
f["value"] = json_optional(value, with_spans)
f["attributes"] = json_nodes(attrs, with_spans)
f["comment"] = json_optional(comment, with_spans)
}
Term(id, value, attrs, comment) => {
f["id"] = id.to_json_ast(with_spans~)
f["value"] = value.to_json_ast(with_spans~)
f["attributes"] = json_nodes(attrs, with_spans)
f["comment"] = json_optional(comment, with_spans)
}
Pattern(elements) => f["elements"] = json_nodes(elements, with_spans)
TextElement(value) | StringLiteral(value) | NumberLiteral(value) =>
f["value"] = Json::string(value)
Placeable(expression) =>
f["expression"] = expression.to_json_ast(with_spans~)
MessageReference(id, attribute) => {
f["id"] = id.to_json_ast(with_spans~)
f["attribute"] = json_optional(attribute, with_spans)
}
TermReference(id, attribute, arguments) => {
f["id"] = id.to_json_ast(with_spans~)
f["attribute"] = json_optional(attribute, with_spans)
f["arguments"] = json_optional(arguments, with_spans)
}
VariableReference(id) => f["id"] = id.to_json_ast(with_spans~)
FunctionReference(id, arguments) => {
f["id"] = id.to_json_ast(with_spans~)
f["arguments"] = arguments.to_json_ast(with_spans~)
}
SelectExpression(selector, variants) => {
f["selector"] = selector.to_json_ast(with_spans~)
f["variants"] = json_nodes(variants, with_spans)
}
CallArguments(positional, named) => {
f["positional"] = json_nodes(positional, with_spans)
f["named"] = json_nodes(named, with_spans)
}
Attribute(id, value) => {
f["id"] = id.to_json_ast(with_spans~)
f["value"] = value.to_json_ast(with_spans~)
}
Variant(key, value, default) => {
f["key"] = key.to_json_ast(with_spans~)
f["value"] = value.to_json_ast(with_spans~)
f["default"] = Json::boolean(default)
}
NamedArgument(name, value) => {
f["name"] = name.to_json_ast(with_spans~)
f["value"] = value.to_json_ast(with_spans~)
}
Identifier(name) => f["name"] = Json::string(name)
Comment(content) | GroupComment(content) | ResourceComment(content) =>
f["content"] = Json::string(content)
Junk(content, annotations) => {
f["content"] = Json::string(content)
f["annotations"] = json_nodes(annotations, with_spans)
}
Annotation(code, args, message) => {
f["code"] = Json::string(code)
f["arguments"] = Json::array(
args.map(a => {
match a {
Some(s) => Json::string(s)
None => Json::null()
}
}),
)
f["message"] = Json::string(message)
}
}
if with_spans && self.span.start >= 0 {
f["span"] = Json::object({
"type": Json::string("Span"),
"start": Json(self.span.start),
"end": Json(self.span.end),
})
}
Json::object(f)
}
///|
/// Structural equality independent of source locations.
pub fn Node::same_syntax(self : Node, other : Node) -> Bool {
self.to_json_ast() == other.to_json_ast()
}
///|
pub extend Span with Eq::{equal, not_equal}
///|
pub extend Syntax with Eq::{equal, not_equal}
///|
pub extend Node with Eq::{equal, not_equal}