// Portions adapted from google/go-jsonnet v0.21.0 (Apache-2.0).
// https://github.com/google/go-jsonnet/tree/67968688d9952f50662e979fab1bef889d0d3e57
// Source: internal/formatter/jsonnetfmt.go
// Source: internal/formatter/unparser.go
// Source: internal/parser/lexer.go
// Copyright 2019 Google Inc. All rights reserved.
// Copyright 2016 Google Inc. All rights reserved.
// Modifications: Implemented formatting controls and structural emission with local syntax containers and budgets.
// License and retained attribution: LICENSE and NOTICE at the module root.
///|
priv struct FormatOutput {
out : StringBuilder
limits : Limits
loc : Location
mut bytes : Int
mut steps : Int
mut line_start : Bool
mut column : Int
}
///|
fn FormatOutput::write(
self : FormatOutput,
text : String,
) -> Unit raise JsonnetError {
let size = @utf8.encode(text).length()
if size > self.limits.output_bytes - self.bytes {
raise problem(Limit, self.loc, "format output byte limit exceeded")
}
self.bytes += size
self.out.write_string(text)
if !text.is_empty() {
self.line_start = text.has_suffix("\n")
for c in text.to_array() {
self.column = if c == '\n' {
0
} else {
self.column + @utf8.encode(c.to_string()).length()
}
}
}
}
///|
fn FormatOutput::tick(self : FormatOutput) -> Unit raise JsonnetError {
self.steps += 1
if self.steps > self.limits.steps {
raise problem(Limit, self.loc, "format step limit exceeded")
}
}
///|
fn FormatOutput::indent(
self : FormatOutput,
spaces : Int,
) -> Unit raise JsonnetError {
if self.line_start {
if spaces > self.limits.output_bytes - self.bytes {
raise problem(Limit, self.loc, "format output byte limit exceeded")
}
for _ in 0.. String raise JsonnetError {
let tokens = format_rewrite(tokens, root, options)
let tokens = if options.sort_imports {
format_sort_imports(tokens)
} else {
tokens
}
let tokens = format_trailing_fodder(tokens)
let writer : FormatOutput = {
out: StringBuilder(),
limits,
loc,
bytes: 0,
steps: 0,
line_start: true,
column: 0,
}
let groups : Array[FormatGroup] = []
let locals : Array[FormatLocal] = []
let layout = format_layout(tokens)
let expression = parse_tokens(tokens, limits.depth, tree_depth~, node_limit?) catch {
Error(d) => raise Error(d)
e => raise problem(Runtime, loc, e.to_string())
}
let indent_plan = format_indent_plan(tokens, expression, layout, writer)
let columns = Array::make(tokens.length(), 0)
let mut previous = ""
let mut ends_expr = false
let mut tight_after = false
let mut first_comment = true
let mut last_line = 0
let assertions : Array[Int] = []
let only_comments = !options.strip_comments && options.strip_all_but_comments
let strip_all = !options.strip_comments &&
!only_comments &&
options.strip_everything
for i, token in tokens {
writer.tick()
let t = token.text
let closing = t == ")" || t == "]" || t == "}"
let mut spaces = if options.indent > 0 && !only_comments {
indent_plan.line_up[i].resolve(
columns,
options.indent,
limits.output_bytes,
)
} else if options.indent > 0 || (layout.newline[i] && !token.has_newline()) {
0
} else {
token.loc.column - 1
}
let block_base = if options.indent > 0 && !only_comments {
indent_plan.base[i].resolve(columns, options.indent, limits.output_bytes)
} else {
spaces
}
if layout.trailing_comma[i] && !only_comments {
writer.write(",")
}
let mut newline_count = 0
let mut interstitial = false
let mut comment_column : Int? = None
for trivia in token.leading {
writer.tick()
match trivia.kind {
Whitespace =>
if !strip_all && !only_comments && !options.strip_comments {
newline_count += trivia.raw
.to_array()
.filter(c => c == '\n')
.length()
}
LineComment | BlockComment => {
if strip_all || options.strip_comments {
continue
}
let paragraph = trivia.kind == BlockComment &&
trivia.raw.contains("\n")
let inline = !paragraph &&
last_line == trivia.range.begin.line &&
last_line > 0
let original_newline = newline_count > 0
if only_comments && inline && trivia.kind == LineComment {
continue
}
if newline_count > 0 {
let count = if options.max_blank_lines > 0 &&
options.max_blank_lines < newline_count - 1 {
options.max_blank_lines + 1
} else {
newline_count
}
if writer.bytes > 0 {
for n in 0.. 0 || !writer.line_start {
writer.write("\n")
}
}
}
newline_count = 0
} else if (only_comments || paragraph) && !writer.line_start {
writer.write("\n")
}
let comment_indent = if options.indent > 0 {
if closing &&
groups.last() is Some({ expanded: true, .. }) &&
spaces <= limits.output_bytes - options.indent {
spaces + options.indent
} else {
spaces
}
} else if paragraph && !original_newline {
// A paragraph that originally flowed after a token acquires an
// implicit line ending. Go takes its indent from the following
// physical line, or zero when that line ending is also implicit.
if trivia.range.end.line == token.loc.line {
0
} else {
token.loc.column - 1
}
} else {
trivia.range.begin.column - 1
}
let follows_comment = interstitial && !writer.line_start
writer.indent(comment_indent)
if follows_comment {
writer.write(" ")
} else if !writer.line_start && !previous.is_empty() && inline {
let crowded = t != ";" &&
t != "," &&
t != "." &&
t != ")" &&
!(t == "(" && ends_expr) &&
!t.has_prefix(":") &&
!t.has_prefix("+:") &&
!(t == "]" && groups.last() is Some({ index: true, .. })) &&
previous != "." &&
previous != "(" &&
(previous != "[" || options.pad_arrays) &&
(previous != "{" || options.pad_objects)
if trivia.kind == LineComment {
writer.write(" ")
} else if crowded {
writer.write(" ")
}
}
let comment = format_comment(
trivia.raw.trim_end().to_owned(),
options.comment_style,
first_comment,
)
if !paragraph && trivia.kind == BlockComment && comment_column is None {
comment_column = Some(writer.column)
}
if paragraph {
for line in format_comment_lines(trivia) {
if !line.is_empty() {
writer.indent(comment_indent)
writer.write(line)
}
writer.write("\n")
}
if options.indent <= 0 && trivia.range.end.line == token.loc.line {
spaces = 0
}
} else {
writer.write(comment)
}
first_comment = false
if trivia.kind == LineComment ||
trivia.raw.contains("\n") ||
only_comments {
if !paragraph {
writer.write("\n")
}
} else {
interstitial = true
}
}
TokenText => ()
}
}
if only_comments {
if !(t == "," && !token.has_newline() && token.loc.line > last_line) {
last_line = token.end.line
}
continue
}
if options.strip_comments {
let lines = format_stripped_lines(
token,
previous.is_empty(),
layout.newline[i],
options.max_blank_lines,
)
for n, (count, original_indent) in lines {
for _ in 0.. 0 { spaces } else { original_indent }
if n + 1 < lines.length() {
writer.indent(indent)
} else if options.indent <= 0 {
spaces = indent
}
}
}
if t == "" {
if !writer.line_start {
writer.write("\n")
}
break
}
if layout.skip[i] {
continue
}
if !strip_all &&
!options.strip_comments &&
layout.newline[i] &&
newline_count == 0 &&
!writer.line_start {
newline_count = 1
}
if newline_count > 0 && (writer.bytes > 0 || !previous.is_empty()) {
let count = if options.max_blank_lines > 0 &&
options.max_blank_lines < newline_count - 1 {
options.max_blank_lines + 1
} else {
newline_count
}
for n in 0.. 0 || !writer.line_start {
writer.write("\n")
}
}
}
let inside = groups.last()
let local_equal = t == "=" &&
locals.last() is Some(scope) &&
scope.equal_pending &&
scope.depth == groups.length()
let compact_equal = t == "=" &&
!local_equal &&
inside is Some({ open: "(", .. })
let prefix = (t == "+" || t == "-" || t == "!" || t == "~") && !ends_expr
let compact_colon = (t == ":" || t == "::") &&
inside is Some({ index: true, .. })
let empty_close = closing &&
(
(previous == "(" && t == ")") ||
(previous == "[" && t == "]") ||
(previous == "{" && t == "}")
)
if t == "assert" {
assertions.push(groups.length())
}
if closing {
while assertions.last() == Some(groups.length()) {
ignore(assertions.pop())
}
}
if (t == "," || t == ";") && assertions.last() == Some(groups.length()) {
ignore(assertions.pop())
}
let assertion_colon = t == ":" && assertions.last() == Some(groups.length())
if assertion_colon {
ignore(assertions.pop())
}
let spaced = if previous.is_empty() || writer.line_start || empty_close {
false
} else if t == "," || t == ";" || t == "." || t == ")" {
false
} else if t == "]" {
inside is Some({ index: false, .. }) && options.pad_arrays
} else if t == "}" {
options.pad_objects
} else if assertion_colon {
true
} else if t == ":" ||
t == "::" ||
t == ":::" ||
t == "+:" ||
t == "+::" ||
t == "+:::" {
false
} else if compact_equal {
false
} else if t == "(" {
if previous == "[" {
inside is Some({ index: false, .. }) && options.pad_arrays
} else {
!ends_expr && previous != "function" && previous != "(" && !tight_after
}
} else if t == "[" && ends_expr {
false
} else if tight_after || previous == "." || previous == "(" {
false
} else if previous == "[" {
inside is Some({ index: false, .. }) && options.pad_arrays
} else if previous == "{" {
options.pad_objects
} else {
true
}
let comment_space = interstitial &&
!writer.line_start &&
!closing &&
t != "," &&
t != ";" &&
!t.has_prefix(":") &&
!t.has_prefix("+:") &&
t != "." &&
!(t == "(" && ends_expr) &&
previous != "."
writer.indent(spaces)
if spaced || comment_space {
writer.write(" ")
}
// Opening interstitial comments belong to the expression's fodder. Their
// width does not move the expression's continuation anchor.
columns[i] = comment_column.unwrap_or(writer.column)
let mut raw = format_string(token, options.string_style)
if options.pretty_field_names &&
token.text == "" &&
tokens.get(i + 1) is Some(next) &&
(
next.text == ":" ||
next.text == "::" ||
next.text == ":::" ||
next.text == "+:" ||
next.text == "+::" ||
next.text == "+:::" ||
(next.text == "(" && layout.parameter[i + 1])
) {
if token.value is Some(String(name)) &&
is_name(name) &&
name.to_array().all(c => identifier_start(c) || digit(c)) {
raw = name
}
}
if t == "" {
writer.write_block(token, options.indent, block_base)
} else {
writer.write(raw)
}
if locals.last() is Some(scope) && scope.depth == groups.length() {
if local_equal {
scope.equal_pending = false
}
if t == ";" || (t == "," && scope.in_object) {
ignore(locals.pop())
} else if t == "," {
scope.equal_pending = true
}
}
if t == "local" {
locals.push({
depth: groups.length(),
in_object: inside is Some({ open: "{", .. }) &&
(previous == "{" || previous == ","),
equal_pending: true,
})
}
if t == "(" || t == "[" || t == "{" {
groups.push({
open: t,
parameter: layout.parameter[i],
index: layout.index[i] || layout.field_key[i],
expanded: layout.expanded[i],
})
ends_expr = false
} else if closing {
let group = groups.pop().unwrap()
ends_expr = !group.parameter
} else {
ends_expr = token.value is Some(_) ||
t == "" ||
is_name(t) ||
t == "true" ||
t == "false" ||
t == "null" ||
t == "self" ||
t == "super" ||
t == "$"
}
tight_after = prefix || compact_equal || compact_colon
previous = t
if !(t == "," && !token.has_newline() && token.loc.line > last_line) {
last_line = token.end.line
}
}
if only_comments {
writer.write("null\n")
}
writer.out.to_string()
}