///|
pub struct Conversion {
regex : @model.Regex?
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}
///|
/// Convert a regex document to the format-independent model. Adjacent
/// characters of a concatenation become one literal term, as PlantUML draws
/// them. Node identities are ordered ownership paths, never labels.
pub fn to_regex(
document : Document,
id? : String = "regex",
limits? : @envelope.Limits = @envelope.Limits::new(),
) -> Conversion {
let nodes : Array[@model.Node] = []
let mut failure : String? = None
fn add(id : String, kind : @model.Kind, span : @diagrams.Span?) -> String {
if nodes.length() >= limits.nodes {
failure = Some("Regex node limit exceeded")
} else {
nodes.push(@model.Node::new(id, kind, source?=span))
}
id
}
fn convert(expr : Expr, id : String, depth : Int) -> String {
if depth > limits.depth {
failure = Some("Regex depth limit exceeded")
return id
}
if failure is Some(_) {
return id
}
match expr {
Char(c, _, span) => add(id, Literal(c), span)
Class(kind, span) => add(id, Class(kind), span)
Backreference(n, span) => add(id, Backreference(n), span)
Set(negated, items, span) => add(id, Set(negated, items), span)
Anchor(kind, span) => add(id, Anchor(kind), span)
Concat(items) => {
// Group the items into merged literal runs and other expressions.
let runs : Array[(Int, Array[Expr])] = []
for i, item in items {
match (item, runs.last()) {
(Char(_, _, _), Some((_, run))) if run.last() is Some(Char(_, _, _)) =>
run.push(item)
_ => runs.push((i, [item]))
}
}
fn literal(run : Array[Expr], id : String) -> String {
let text = StringBuilder()
let mut first : @diagrams.Span? = None
let mut last : @diagrams.Span? = None
for e in run {
if e is Char(c, _, span) {
text.write_string(c)
if first is None {
first = span
}
last = span
}
}
let span = match (first, last) {
(Some(a), Some(b)) => Some(@diagrams.Span::{ start: a.start, end: b.end })
_ => None
}
add(id, Literal(text.to_string()), span)
}
if runs.length() == 1 {
let (_, run) = runs[0]
return literal(run, id)
}
let children = []
for pair in runs {
let (index, run) = pair
let child = id + "." + index.to_string()
if run.length() > 1 || run[0] is Char(_, _, _) {
children.push(literal(run, child))
} else {
children.push(convert(run[0], child, depth + 1))
}
}
add(id, Sequence(ReadOnlyArray::from_array(children)), None)
}
Alt(items) => {
let children = []
for i, item in items {
children.push(convert(item, id + "." + i.to_string(), depth + 1))
}
add(id, Choice(ReadOnlyArray::from_array(children)), None)
}
Group(kind, inner, span) => {
let child = convert(inner, id + ".0", depth + 1)
add(id, Group(kind, child), span)
}
Look(look, positive, inner, span) => {
let child = convert(inner, id + ".0", depth + 1)
add(id, Assertion(look, positive, child), span)
}
Quantified(inner, bounds, span) => {
let child = convert(inner, id + ".0", depth + 1)
add(id, Repeat(bounds, child), span)
}
}
}
let root = convert(document.pattern, "p", 0)
if failure is Some(message) {
return {
regex: None,
diagnostics: [@diagrams.Diagnostic::error("regex/limit", message)],
}
}
let mut title = None
for statement in document.statements {
if statement is Title(text) {
title = Some(text)
}
}
let model = @model.Regex::new(
id,
root,
nodes,
title?,
pattern?=if document.text.is_empty() { None } else { Some(document.text) },
)
let findings = @model.validate(
model,
limits=@model.Limits::new(
nodes=limits.nodes,
depth=limits.depth + 1,
work=limits.work,
),
)
if !findings.is_empty() {
return { regex: None, diagnostics: findings }
}
{ regex: Some(model), diagnostics: [] }
}