///|
pub struct ReadResult {
nodes : ReadOnlyArray[@cst.Node]
document : Document?
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}
///|
pub let diagnostic_codes : ReadOnlyArray[String] = [
"state/syntax", "state/unclosed-composite", "state/unexpected-close", "state/limit",
"state/refused-scope", "state/invalid-ast", "state/unapplied-presentation", "state/external-asset",
]
///|
fn trim(text : String) -> String {
@envelope.trim(text)
}
///|
fn part(text : String, a : Int32, b : Int32) -> String {
@cst.slice(text, a, b)
}
///|
fn after(text : String, prefix : String) -> String {
trim(part(text, prefix.length(), text.length()))
}
///|
fn outside(text : String, needle : Int32) -> Int32? {
let mut quote = false
let mut escaped = false
for i = 0; i < text.length(); i = i + 1 {
let c = text.code_unit_at(i).to_int()
if c == 34 && !escaped {
quote = !quote
}
if !quote && c == needle {
return Some(i)
}
escaped = c == 92 && !escaped
}
None
}
///|
fn unquote(text : String) -> String {
if text.length() >= 2 && text.has_prefix("\"") && text.has_suffix("\"") {
part(text, 1, text.length() - 1)
.replace_all(old="\\n", new="\n")
.replace_all(old="\\\"", new="\"")
.replace_all(old="\\\\", new="\\")
} else {
text
}
}
///|
fn quote_end(text : String) -> Int32? {
let mut escaped = false
for i = 1; i < text.length(); i = i + 1 {
let c = text.code_unit_at(i).to_int()
if c == 34 && !escaped {
return Some(i)
}
escaped = c == 92 && !escaped
}
None
}
///|
fn identifier(text : String) -> Bool {
if ["[*]", "[H]", "[H*]"].contains(text) {
return true
}
let mut name = text
if name.has_suffix("[H*]") {
name = part(name, 0, name.length() - 4)
} else if name.has_suffix("[H]") {
name = part(name, 0, name.length() - 3)
}
if name.has_prefix("==") && name.has_suffix("==") {
name = name.trim_start(chars="=").trim_end(chars="=").to_owned()
}
if name.is_empty() {
return false
}
for c in name {
if c.is_whitespace() || c.is_control() {
return false
}
if c.is_ascii() &&
!(c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '_' || c == '.') {
return false
}
}
true
}
///|
fn declaration(text : String) -> Kind? {
let mut t = after(text, "state")
let composite = t.has_suffix("{")
if composite {
t = trim(part(t, 0, t.length() - 1))
}
let mut decoration = ""
if outside(t, 60) is Some(at) {
decoration = trim(part(t, at, t.length()))
t = trim(part(t, 0, at))
} else if outside(t, 35) is Some(at) {
decoration = trim(part(t, at, t.length()))
t = trim(part(t, 0, at))
}
let mut code = ""
let mut label = ""
if t.has_prefix("\"") {
guard quote_end(t) is Some(end) else { return None }
label = unquote(part(t, 0, end + 1))
let tail = trim(part(t, end + 1, t.length()))
if tail.has_prefix("as ") {
code = after(tail, "as")
} else if tail.is_empty() {
code = label
} else {
return None
}
} else if t.contains(" as ") {
let at = t.find(" as ").unwrap()
code = trim(part(t, 0, at))
label = unquote(trim(part(t, at + 4, t.length())))
} else {
code = t
label = t.split(".").to_array().last().unwrap_or(t[:]).to_owned()
}
if !identifier(code) {
None
} else {
Some(Declaration(code, label, decoration, composite))
}
}
///|
fn direction_word(value : String) -> @flowchart.Direction? {
match value {
"l" | "le" | "left" => Some(RL)
"r" | "ri" | "right" => Some(LR)
"u" | "up" => Some(BT)
"d" | "do" | "down" => Some(TB)
_ => None
}
}
///|
fn arrow_hint(arrow : String) -> @flowchart.Direction? {
let word = StringBuilder()
let mut bracket = 0
for i = 0; i < arrow.length(); i = i + 1 {
let c = arrow.code_unit_at(i).to_int()
if c == 91 {
bracket += 1
}
if c == 93 {
bracket -= 1
}
if bracket == 0 && c >= 97 && c <= 122 {
if (c == 120 || c == 111) && (i == 0 || i + 1 == arrow.length()) {
continue
}
word.write_string(part(arrow, i, i + 1))
}
}
direction_word(word.to_string())
}
///|
fn transition(text : String) -> Kind? {
let mut t = text
let mut label = ""
if outside(t, 58) is Some(colon) {
label = trim(part(t, colon + 1, t.length()))
t = trim(part(t, 0, colon))
}
let mut a = -1
let mut quote = false
let mut bracket = 0
for i = 0; i < t.length(); i = i + 1 {
let c = t.code_unit_at(i).to_int()
if c == 34 {
quote = !quote
}
if quote {
continue
}
if c == 91 {
bracket += 1
}
if c == 93 {
bracket -= 1
}
if bracket > 0 {
continue
}
if c == 45 ||
c == 60 ||
(c == 46 && i + 1 < t.length() && t.code_unit_at(i + 1).to_int() == 46) {
a = i
break
}
}
guard a > 0 else { return None }
let reverse = t.code_unit_at(a).to_int() == 60
let mut b = a
let mut i = a
// Consume arrow grammar only. Target dots and direction-like names never
// extend the token after its arrowhead or final reverse body dash.
while i < t.length() {
let c = t.code_unit_at(i).to_int()
if c == 62 {
b = i + 1
break
}
if c == 45 || (c == 60 && i == a) || (c == 46 && !reverse) {
b = i + 1
i += 1
continue
}
if c == 91 {
let mut end = i + 1
while end < t.length() && t.code_unit_at(end).to_int() != 93 {
end += 1
}
if end >= t.length() {
return None
}
i = end + 1
continue
}
let mut end = i
while end < t.length() &&
t.code_unit_at(end).to_int() >= 97 &&
t.code_unit_at(end).to_int() <= 122 {
end += 1
}
let direction = part(t, i, end)
if end > i &&
direction_word(direction) != None &&
end < t.length() &&
(t.code_unit_at(end).to_int() == 45 || t.code_unit_at(end).to_int() == 91) {
i = end
continue
}
break
}
guard b > a else { return None }
// The pinned circle decoration requires whitespace after `o`; otherwise
// a compact target such as `open` starts at that code unit.
if b + 1 < t.length() &&
t.code_unit_at(b).to_int() == 111 &&
[32, 9].contains(t.code_unit_at(b + 1).to_int()) {
b += 1
}
// With no separating space the final `x` belongs to an alias such as Box.
if a >= 2 &&
t.code_unit_at(a - 1).to_int() == 120 &&
[32, 9].contains(t.code_unit_at(a - 2).to_int()) {
a -= 1
}
let from = trim(part(t, 0, a))
let arrow = trim(part(t, a, b))
let target = trim(part(t, b, t.length()))
if identifier(from) &&
identifier(target) &&
(
arrow.contains(">") ||
arrow.contains("<") ||
arrow == "--" ||
arrow == ".."
) {
Some(Transition(from, arrow, target, label))
} else {
None
}
}
///|
/// Read a scanned native state block. Recovery preserves subsequent statements.
pub fn read(
scan : @envelope.Scan,
limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> ReadResult {
let diagnostics = scan.diagnostics.iter().collect()
let items : Array[Statement] = []
let nodes = scan.line_nodes(line => {
Some(@cst.Node::new(Data, line.tokens(Text)))
})
let mut depth = 0
let mut regions = 1
let mut transitions = 0
let mut declarations = 0
let mut work = 0
let mut i = 0
fn error(code : String, text : String, span : @diagrams.Span?) {
if diagnostics.length() < limits.diagnostics.max(1) {
diagnostics.push(@diagrams.Diagnostic::error(code, text, span?))
}
}
fn emit(kind : Kind, span : @diagrams.Span) {
items.push(Statement::new(kind, span~))
}
while i < scan.lines.length() {
let line = scan.lines[i]
i += 1
if line.role != Body {
continue
}
let mut text = trim(line.text)
let span = scan.span(line.offset, line.offset + line.text.length())
work += text.length() + 1
if work > limits.work || items.length() >= limits.nodes || !limits.valid() {
error(
"state/limit",
"State reading work or statement limit exceeded",
Some(span),
)
break
}
if text.is_empty() {
continue
}
if outside(text, 39) is Some(at) {
text = trim(part(text, 0, at))
}
if text.is_empty() {
continue
}
if text.contains("
limits.work {
error("state/limit", "Note work limit exceeded", Some(span))
break
}
if trim(t.text) == "end note" || trim(t.text) == "endnote" {
closed = true
break
}
lines.push(t.text)
}
content = lines.join("\n")
if !closed {
error("state/syntax", "Unclosed note", Some(span))
}
}
let placement = if header.has_prefix("note left") {
"left"
} else if header.has_prefix("note top") {
"top"
} else if header.has_prefix("note bottom") {
"bottom"
} else {
"right"
}
if header.has_prefix("note on link") {
target = Some("@link")
}
if header.find(" of ") is Some(at) {
target = Some(trim(part(header, at + 4, header.length())))
}
if content.contains("
limits.regions {
error("state/limit", "Region count limit exceeded", Some(span))
break
}
emit(Separator(text == "||"), span)
} else if text == "left to right direction" {
emit(Direction(LR), span)
} else if text == "top to bottom direction" {
emit(Direction(TB), span)
} else if text.to_lower().has_prefix("state ") {
let mut declaration_text = text
let mut description : String? = None
if outside(text, 58) is Some(at) {
// Color specifications contain colons until a separating space-colon.
if !text.contains("#") ||
(at > 0 && text.code_unit_at(at - 1).to_int() == 32) {
declaration_text = trim(part(text, 0, at))
description = Some(trim(part(text, at + 1, text.length())))
} else if text.find(" : ") is Some(at) {
declaration_text = trim(part(text, 0, at))
description = Some(trim(part(text, at + 3, text.length())))
}
}
match
declaration(
"state " + part(declaration_text, 6, declaration_text.length()),
) {
Some(kind) => {
declarations += 1
if kind is Declaration(_, _, decoration, composite) {
if composite {
depth += 1
regions += 1
}
if !decoration.is_empty() &&
diagnostics.length() < limits.diagnostics {
diagnostics.push(
@diagrams.Diagnostic::warning(
"state/unapplied-presentation",
"Stereotypes and colors remain inert",
span~,
),
)
}
}
if depth > limits.depth ||
declarations > limits.nodes ||
regions > limits.regions {
error(
"state/limit",
"State count or nesting limit exceeded",
Some(span),
)
break
}
let declaration_body = after(declaration_text, "state")
let explicit_label = declaration_body.has_prefix("\"") ||
declaration_body.contains(" as ")
items.push(Statement::new(kind, span~, explicit_label~))
if description is Some(description) &&
kind is Declaration(code, _, _, _) {
emit(Description(code, description), span)
}
}
None => error("state/syntax", "Invalid state declaration", Some(span))
}
} else if text.has_prefix("mainframe ") {
emit(Presentation(text), span)
if diagnostics.length() < limits.diagnostics {
diagnostics.push(
@diagrams.Diagnostic::warning(
"state/unapplied-presentation",
"Mainframe presentation is retained but unapplied",
span~,
),
)
}
} else if transition(text) is Some(kind) {
transitions += 1
if transitions > limits.edges {
error("state/limit", "Transition limit exceeded", Some(span))
break
}
emit(kind, span)
} else if outside(text, 58) is Some(at) {
let code = trim(part(text, 0, at))
if identifier(code) {
emit(Description(code, trim(part(text, at + 1, text.length()))), span)
} else {
error("state/syntax", "Invalid description target", Some(span))
}
} else {
error("state/syntax", "Unknown state statement: " + text, Some(span))
}
}
if depth > 0 {
error("state/unclosed-composite", "Unclosed composite state", None)
}
if diagnostics.length() >= limits.diagnostics {
diagnostics.push(
@diagrams.Diagnostic::error("state/limit", "Diagnostic limit reached"),
)
}
{
nodes: ReadOnlyArray::from_array(nodes),
document: Some(
Document::new(
items[:],
name?=scan.name,
complete=!diagnostics.iter().any(d => d.severity == Error),
statements=scan.statements[:],
),
),
diagnostics: ReadOnlyArray::from_array(diagnostics),
}
}