///|
/// Write a JSON string with its quotes. `%` before a name character is
/// written as `%`, so the line never looks like a preprocessor call.
fn write_string(out : StringBuilder, text : String) -> Unit {
let n = text.length()
out.write_char('"')
let mut run = 0
fn flush(at : Int32) {
out.write_string(@cst.slice(text, run, at))
}
for i in 0.. Some("\\\"")
92 => Some("\\\\")
8 => Some("\\b")
12 => Some("\\f")
10 => Some("\\n")
13 => Some("\\r")
9 => Some("\\t")
37 => {
let next = if i + 1 < n { text.code_unit_at(i + 1).to_int() } else { 0 }
if (next >= 97 && next <= 122) ||
(next >= 65 && next <= 90) ||
next == 95 {
Some("\\u0025")
} else {
None
}
}
_ =>
if c < 32 ||
(c >= 0xd800 && c <= 0xdbff && !paired_after(text, i)) ||
(c >= 0xdc00 && c <= 0xdfff && !paired_before(text, i)) {
Some("\\u" + hex4(c))
} else {
None
}
}
if escape is Some(spelling) {
flush(i)
out.write_string(spelling)
run = i + 1
}
}
flush(n)
out.write_char('"')
}
///|
fn paired_after(text : String, i : Int32) -> Bool {
i + 1 < text.length() &&
text.code_unit_at(i + 1).to_int() >= 0xdc00 &&
text.code_unit_at(i + 1).to_int() <= 0xdfff
}
///|
fn paired_before(text : String, i : Int32) -> Bool {
i > 0 &&
text.code_unit_at(i - 1).to_int() >= 0xd800 &&
text.code_unit_at(i - 1).to_int() <= 0xdbff
}
///|
fn hex4(c : Int32) -> String {
let digits = "0123456789abcdef"
let out = StringBuilder()
for shift in [12, 8, 4, 0] {
let d = (c >> shift) & 15
out.write_string(@cst.slice(digits, d, d + 1))
}
out.to_string()
}
///|
priv enum Step {
Open(Value, Int32)
Close(Bool, Int32)
Separator
Name(String, Int32)
Line(Int32)
}
///|
/// Write the canonical text of one value: two-space indentation, one member
/// or element on each line, `{}` and `[]` for empty containers. The output
/// has no trailing line break. Numbers keep their exact lexemes.
pub fn print_value(
value : Value,
limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> Result[String, ReadOnlyArray[@diagrams.Diagnostic]] {
if !limits.valid() {
return Err([
@diagrams.Diagnostic::error(
"json/invalid-limits", "JSON limits must be positive",
),
])
}
let out = StringBuilder()
let mut units = 0
let mut work = 0
let stack : Array[Step] = [Open(value, 0)]
fn indent(depth : Int32) -> String {
String::make(depth * 2, ' ')
}
fn error(
code : String,
message : String,
) -> Result[String, ReadOnlyArray[@diagrams.Diagnostic]] {
Err([@diagrams.Diagnostic::error(code, message)])
}
fn emit(text : String) -> Bool {
if text.length() > limits.output - units {
return false
}
units += text.length()
out.write_string(text)
true
}
let output_failure = "Canonical JSON exceeds the output limit"
while stack.pop() is Some(step) {
work += 1
if work > limits.work {
return error("json/work-limit", "JSON writing exceeds its work limit")
}
match step {
Separator =>
if !emit(",") {
return error("json/output-limit", output_failure)
}
Close(object, depth) =>
if !emit("\n" + indent(depth) + (if object { "}" } else { "]" })) {
return error("json/output-limit", output_failure)
}
Line(depth) =>
if !emit("\n" + indent(depth)) {
return error("json/output-limit", output_failure)
}
Name(name, depth) => {
let part = StringBuilder()
part.write_char('\n')
part.write_string(indent(depth))
write_string(part, name)
part.write_string(": ")
if !emit(part.to_string()) {
return error("json/output-limit", output_failure)
}
}
Open(value, depth) => {
let text = match value.kind {
String(s) => {
let part = StringBuilder()
write_string(part, s.text)
Some(part.to_string())
}
Number(lexeme) =>
if @data_tree.valid_number(lexeme) {
Some(lexeme)
} else {
return error(
"json/invalid-model",
"'\{lexeme}' is not a valid JSON number lexeme",
)
}
Boolean(true) => Some("true")
Boolean(false) => Some("false")
Null => Some("null")
Object(members) if members.is_empty() => Some("{}")
Array(items) if items.is_empty() => Some("[]")
_ => None
}
match text {
Some(text) =>
if !emit(text) {
return error("json/output-limit", output_failure)
}
None => {
if depth >= limits.depth {
return error(
"json/depth-limit",
"JSON nesting exceeds the depth limit of \{limits.depth}",
)
}
match value.kind {
Object(members) => {
if !emit("{") {
return error("json/output-limit", output_failure)
}
stack.push(Close(true, depth))
for i in 0.. 0 {
stack.push(Separator)
}
}
}
Array(items) => {
if !emit("[") {
return error("json/output-limit", output_failure)
}
stack.push(Close(false, depth))
for i in 0.. 0 {
stack.push(Separator)
}
}
}
_ => ()
}
}
}
}
}
}
Ok(out.to_string())
}
///|
/// Write the canonical body of a JSON block: one `#highlight` line for each
/// selector, then the value. Every line ends with a line break. The text has
/// no block markers and no shared statements. On error there is no output.
pub fn print_body(
document : Document,
limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> Result[String, ReadOnlyArray[@diagrams.Diagnostic]] {
let out = StringBuilder()
let mut units = 0
for selector in document.highlights {
let line = selector.to_line()
match @data.parse_selector(line) {
Ok(parsed) if parsed.logical_equal(selector) => ()
_ =>
return Err([
@diagrams.Diagnostic::error(
"json/invalid-model", "A highlight selector cannot be written in the native profile",
),
])
}
units += line.length() + 1
out.write_string(line)
out.write_char('\n')
}
if units >= limits.output - 1 {
return Err([
@diagrams.Diagnostic::error(
"json/output-limit", "Canonical JSON exceeds the output limit",
),
])
}
let value = print_value(
document.value,
limits=@envelope.Limits::new(
source=limits.source,
output=limits.output - units - 1,
depth=limits.depth,
nodes=limits.nodes,
work=limits.work,
diagnostics=limits.diagnostics,
),
)
match value {
Err(diagnostics) => Err(diagnostics)
Ok(text) => {
out.write_string(text)
out.write_char('\n')
Ok(out.to_string())
}
}
}