///|
/// The stable diagnostic codes of the JSON reader, writer and converter.
pub let diagnostic_codes : ReadOnlyArray[String] = [
"json/invalid-string", "json/invalid-escape", "json/invalid-number", "json/invalid-literal",
"json/unexpected-character", "json/unexpected-token", "json/unexpected-comment",
"json/missing-value", "json/trailing-content", "json/depth-limit", "json/node-limit",
"json/work-limit", "json/output-limit", "json/invalid-limits", "json/diagnostic-limit",
"json/invalid-highlight", "json/unresolved-highlight", "json/ambiguous-highlight",
"json/unapplied-highlight-style", "json/invalid-model",
]
///|
priv enum Lexeme {
Open(Bool)
Close(Bool)
Colon
Comma
Text(String)
Num(String)
Lit(ValueKind)
Comment
Bad
End
}
///|
/// One significant token for the parser. `object` in `Open` and `Close` is
/// true for braces and false for brackets.
priv struct Tok {
lexeme : Lexeme
start : Int32
end : Int32
text : String
}
///|
priv struct Lexer {
toks : Array[Tok]
issues : Array[(String, String, Int32, Int32)]
}
///|
fn is_hex(c : Int32) -> Bool {
(c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70)
}
///|
fn hex_value(c : Int32) -> Int32 {
if c <= 57 {
c - 48
} else if c >= 97 {
c - 87
} else {
c - 55
}
}
///|
fn is_number_unit(c : Int32) -> Bool {
(c >= 48 && c <= 57) || c == 45 || c == 43 || c == 46 || c == 101 || c == 69
}
///|
fn is_word_unit(c : Int32) -> Bool {
(c >= 97 && c <= 122) ||
(c >= 65 && c <= 90) ||
(c >= 48 && c <= 57) ||
c == 95
}
///|
/// Decode the string token that starts at `start` (a quote). The result is
/// the end offset, the decoded text and the first problem, if any.
fn decode_string(
text : String,
start : Int32,
) -> (Int32, String, (String, String)?) {
let n = text.length()
let out = StringBuilder()
let mut problem : (String, String)? = None
let mut i = start + 1
let mut run = i
while i < n {
let c = text.code_unit_at(i).to_int()
if c == 34 {
out.write_string(@cst.slice(text, run, i))
return (i + 1, out.to_string(), problem)
}
if c == 92 {
out.write_string(@cst.slice(text, run, i))
let e = if i + 1 < n { text.code_unit_at(i + 1).to_int() } else { -1 }
match e {
34 | 47 | 92 => {
out.write_char(e.unsafe_to_char())
i += 2
}
98 => {
out.write_char('\b')
i += 2
}
102 => {
out.write_char('\u{0C}')
i += 2
}
110 => {
out.write_char('\n')
i += 2
}
114 => {
out.write_char('\r')
i += 2
}
116 => {
out.write_char('\t')
i += 2
}
117 => {
let mut value = 0
let mut ok = true
for k in 0..<4 {
let at = i + 2 + k
if at >= n || !is_hex(text.code_unit_at(at).to_int()) {
ok = false
break
}
value = value * 16 + hex_value(text.code_unit_at(at).to_int())
}
if ok {
// A lone surrogate stays one UTF-16 code unit, as upstream.
out.write_char(value.unsafe_to_char())
i += 6
} else {
if problem is None {
problem = Some(
(
"json/invalid-escape", "A \\u escape needs four hexadecimal digits",
),
)
}
i += 2
}
}
_ => {
if problem is None {
problem = Some(
(
"json/invalid-escape", "Invalid escape sequence; use \\\" \\\\ \\/ \\b \\f \\n \\r \\t or \\uXXXX",
),
)
}
i += if e == -1 { 1 } else { 2 }
}
}
run = i
continue
}
if c < 32 {
if problem is None {
problem = Some(
(
"json/invalid-string", "A JSON string cannot contain a control character; use an escape",
),
)
}
}
i += 1
}
out.write_string(@cst.slice(text, run, n))
(
n,
out.to_string(),
Some(("json/invalid-string", "The string has no closing quote on its line")),
)
}
///|
/// Tokenize one body line. Tokens never cross a line: strings cannot hold a
/// raw line break and a `//` comment ends at the line break.
fn Lexer::line(self : Lexer, line : @envelope.Line) -> @cst.Node {
let text = line.text
let base = line.offset
let n = text.length()
let children : Array[@cst.Element] = []
let mut blank = true
fn token(kind : @cst.TokenKind, start : Int32, end : Int32) {
children.push(
Token(@cst.Token::new(kind, @cst.slice(text, start, end), base + start)),
)
}
fn tok(lexeme : Lexeme, start : Int32, end : Int32) {
self.toks.push({
lexeme,
start: base + start,
end: base + end,
text: @cst.slice(text, start, end),
})
}
fn issue(code : String, message : String, start : Int32, end : Int32) {
self.issues.push((code, message, base + start, base + end))
}
let mut i = 0
while i < n {
let c = text.code_unit_at(i).to_int()
let start = i
if c == 32 || c == 9 {
while i < n &&
(
text.code_unit_at(i).to_int() == 32 ||
text.code_unit_at(i).to_int() == 9
) {
i += 1
}
token(Whitespace, start, i)
continue
}
blank = false
match c {
123 | 91 => {
i += 1
token(Punctuation, start, i)
tok(Open(c == 123), start, i)
}
125 | 93 => {
i += 1
token(Punctuation, start, i)
tok(Close(c == 125), start, i)
}
58 => {
i += 1
token(Punctuation, start, i)
tok(Colon, start, i)
}
44 => {
i += 1
token(Punctuation, start, i)
tok(Comma, start, i)
}
34 => {
let (end, decoded, problem) = decode_string(text, start)
i = end
match problem {
None => {
token(String, start, i)
tok(Text(decoded), start, i)
}
Some((code, message)) => {
token(Error, start, i)
tok(Bad, start, i)
issue(code, message, start, i)
}
}
}
47 =>
if i + 1 < n && text.code_unit_at(i + 1).to_int() == 47 {
i = n
token(Comment, start, i)
tok(Comment, start, i)
} else {
i += 1
token(Error, start, i)
tok(Bad, start, i)
issue(
"json/unexpected-character", "A comment starts with '//'", start, i,
)
}
_ =>
if c == 45 || (c >= 48 && c <= 57) {
while i < n && is_number_unit(text.code_unit_at(i).to_int()) {
i += 1
}
let lexeme = @cst.slice(text, start, i)
if @data_tree.valid_number(lexeme) {
token(Number, start, i)
tok(Num(lexeme), start, i)
} else {
token(Error, start, i)
tok(Bad, start, i)
issue(
"json/invalid-number",
"'\{lexeme}' is not a valid JSON number",
start,
i,
)
}
} else if is_word_unit(c) {
while i < n && is_word_unit(text.code_unit_at(i).to_int()) {
i += 1
}
let word = @cst.slice(text, start, i)
let literal : ValueKind? = match word {
"true" => Some(Boolean(true))
"false" => Some(Boolean(false))
"null" => Some(Null)
_ => None
}
match literal {
Some(kind) => {
token(Literal, start, i)
tok(Lit(kind), start, i)
}
None => {
token(Error, start, i)
tok(Bad, start, i)
issue(
"json/invalid-literal",
"'\{word}' is not a JSON value; use true, false, null, a number or a quoted string",
start,
i,
)
}
}
} else {
i += if c >= 0xd800 && c <= 0xdbff && i + 1 < n { 2 } else { 1 }
token(Error, start, i)
tok(Bad, start, i)
issue(
"json/unexpected-character",
"Unexpected character '\{@cst.slice(text, start, i)}'",
start,
i,
)
}
}
}
if !line.newline.is_empty() {
children.push(Token(@cst.Token::new(Newline, line.newline, base + n)))
}
@cst.Node::new(if blank { Blank } else { Data }, children)
}
///|
/// The result of reading a JSON body.
pub struct ReadResult {
/// The body nodes in source order, with shared statement nodes.
nodes : Array[@cst.Node]
document : Document?
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}
///|
priv struct Frame {
object : Bool
start : Int32
members : Array[Member]
items : Array[Value]
mut name : Str
mut name_start : Int32
}
///|
priv enum Mode {
NeedValue
NeedName
After(Value)
}
///|
/// Read the JSON body of a scanned block. The reader keeps every member
/// occurrence in order, exact number lexemes and decoded strings with their
/// spellings. It follows the pinned PlantUML profile: a `//` comment can
/// stand only before a member name or after a member value; malformed input
/// is refused with a diagnostic, never coerced.
pub fn read(
scan : @envelope.Scan,
limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> ReadResult {
let diagnostics = scan.diagnostics.iter().to_array()
let lexer : Lexer = { toks: [], issues: [], }
let nodes = scan.line_nodes(line => Some(lexer.line(line)))
fn report(diagnostic : @diagrams.Diagnostic) {
if diagnostics.length() < limits.diagnostics {
diagnostics.push(diagnostic)
} else if diagnostics.length() == limits.diagnostics {
diagnostics.push(
@diagrams.Diagnostic::error(
"json/diagnostic-limit", "Diagnostic limit reached",
),
)
}
}
fn done(document : Document?) -> ReadResult {
let failed = diagnostics.iter().any(d => d.severity == Error)
{
nodes,
document: if failed {
None
} else {
document
},
diagnostics: ReadOnlyArray::from_array(diagnostics),
}
}
if !limits.valid() {
report(
@diagrams.Diagnostic::error(
"json/invalid-limits", "JSON limits must be positive",
),
)
return done(None)
}
for issue in lexer.issues {
let (code, message, start, end) = issue
report(
@diagrams.Diagnostic::error(code, message, span=scan.span(start, end)),
)
}
let (selectors, highlight_issues) = @data.read_selectors(scan, "json")
for diagnostic in highlight_issues {
report(diagnostic)
}
let toks = lexer.toks
let end_offset = scan.lines
.iter()
.fold(init=0, (at, line) => {
if line.role is Body {
line.offset + line.text.length()
} else {
at
}
})
let mut pos = 0
let mut work = 0
let mut count = 0
let end_tok : Tok = {
lexeme: End,
start: end_offset,
end: end_offset,
text: "",
}
fn next() -> Tok {
if pos < toks.length() {
pos += 1
toks[pos - 1]
} else {
end_tok
}
}
fn peek() -> Tok {
if pos < toks.length() {
toks[pos]
} else {
end_tok
}
}
fn fail(tok : Tok, code : String, message : String) -> ReadResult {
// A lexical error already has its own diagnostic.
if !(tok.lexeme is Bad) {
report(
@diagrams.Diagnostic::error(
code,
message,
span=scan.span(tok.start, tok.end),
),
)
}
done(None)
}
fn describe(tok : Tok) -> String {
match tok.lexeme {
End => "the end of the data"
_ => "'\{tok.text}'"
}
}
let stack : Array[Frame] = []
let mut mode = NeedValue
let mut root : Value? = None
while root is None {
work += 1
if work > limits.work {
report(
@diagrams.Diagnostic::error(
"json/work-limit", "JSON reading exceeds its work limit",
),
)
return done(None)
}
match mode {
NeedValue => {
let tok = next()
let kind : ValueKind? = match tok.lexeme {
Text(decoded) => Some(String({ text: decoded, raw: Some(tok.text), }))
Num(lexeme) => Some(Number(lexeme))
Lit(kind) => Some(kind)
_ => None
}
count += 1
if count > limits.nodes {
report(
@diagrams.Diagnostic::error(
"json/node-limit",
"JSON data exceeds the node limit",
span=scan.span(tok.start, tok.end),
),
)
return done(None)
}
match kind {
Some(kind) =>
mode = After({ kind, span: Some(scan.span(tok.start, tok.end)), })
None =>
match tok.lexeme {
Open(object) => {
if stack.length() >= limits.depth {
report(
@diagrams.Diagnostic::error(
"json/depth-limit",
"JSON nesting exceeds the depth limit of \{limits.depth}",
span=scan.span(tok.start, tok.end),
),
)
return done(None)
}
stack.push({
object,
start: tok.start,
members: [],
items: [],
name: Str::new(""),
name_start: tok.start,
})
if peek().lexeme is Close(closing) && closing == object {
let close = next()
let frame = stack.unsafe_pop()
let kind : ValueKind = if object {
Object([])
} else {
Array([])
}
mode = After({
kind,
span: Some(scan.span(frame.start, close.end)),
})
} else {
mode = if object { NeedName } else { NeedValue }
}
}
End =>
return fail(
tok,
"json/missing-value",
if stack.is_empty() {
"A JSON block needs a value"
} else {
"Expected a value before the end of the data"
},
)
Comment =>
return fail(
tok, "json/unexpected-comment", "A comment cannot stand here; PlantUML allows one only before a member name or after a member value",
)
_ =>
return fail(
tok,
"json/unexpected-token",
"Expected a value, found \{describe(tok)}",
)
}
}
}
NeedName => {
let mut tok = next()
if tok.lexeme is Comment {
tok = next()
}
guard tok.lexeme is Text(decoded) else {
return fail(
tok,
if tok.lexeme is Comment {
"json/unexpected-comment"
} else {
"json/unexpected-token"
},
"Expected a quoted member name, found \{describe(tok)}",
)
}
let colon = next()
guard colon.lexeme is Colon else {
return fail(
colon,
"json/unexpected-token",
"Expected ':' after the member name, found \{describe(colon)}",
)
}
let frame = stack[stack.length() - 1]
frame.name = { text: decoded, raw: Some(tok.text), }
frame.name_start = tok.start
mode = NeedValue
}
After(value) => {
guard stack.last() is Some(frame) else {
root = Some(value)
break
}
if frame.object {
frame.members.push({
name: frame.name,
value,
span: Some(
scan.span(
frame.name_start,
value.span.map(s => s.end.offset).unwrap_or(frame.name_start),
),
),
})
if peek().lexeme is Comment {
ignore(next())
}
} else {
frame.items.push(value)
}
let tok = next()
match tok.lexeme {
Comma => mode = if frame.object { NeedName } else { NeedValue }
Close(closing) if closing == frame.object => {
ignore(stack.pop())
let kind : ValueKind = if frame.object {
Object(ReadOnlyArray::from_array(frame.members))
} else {
Array(ReadOnlyArray::from_array(frame.items))
}
mode = After({ kind, span: Some(scan.span(frame.start, tok.end)), })
}
Comment =>
return fail(
tok, "json/unexpected-comment", "A comment cannot stand here; PlantUML allows one only before a member name or after a member value",
)
_ =>
return fail(
tok,
"json/unexpected-token",
"Expected ',' or '\{if frame.object { "}" } else { "]" }}', found \{describe(tok)}",
)
}
}
}
}
let rest = next()
if !(rest.lexeme is End) {
return fail(
rest,
"json/trailing-content",
"Unexpected \{describe(rest)} after the JSON value",
)
}
guard root is Some(value) else { return done(None) }
done(
Some({
name: scan.name,
statements: scan.statements,
highlights: ReadOnlyArray::from_array(selectors),
value,
}),
)
}