/*
* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
* This source file is part of the Cangjie project, licensed under Apache-2.0
* with Runtime Library Exception.
*
* See https://cangjie-lang.cn/pages/LICENSE for license information.
*/
// The Cangjie API is in Beta. For details on its capabilities and limitations, please refer to the README file.
package std.unittest.common
import std.collection.ArrayList
/**
* Color of the pretty output for tests
*/
public enum Color <: Equatable<Color> {
| RED
| GREEN
| YELLOW
| BLUE
| CYAN
| MAGENTA
| GRAY
| DEFAULT_COLOR
public operator func ==(that: Color): Bool {
match (this) {
case RED => if (let RED <- that) { true } else { false }
case GREEN => if (let GREEN <- that) { true } else { false }
case YELLOW => if (let YELLOW <- that) { true } else { false }
case BLUE => if (let BLUE <- that) { true } else { false }
case CYAN => if (let CYAN <- that) { true } else { false }
case MAGENTA => if (let MAGENTA <- that) { true } else { false }
case GRAY => if (let GRAY <- that) { true } else { false }
case DEFAULT_COLOR => if (let DEFAULT_COLOR <- that) { true } else { false }
}
}
public operator func !=(that: Color): Bool { !(this == that) }
}
/**
* Abstract class to print your output to with support for indentation, alignment and colors
*/
public abstract class PrettyPrinter {
/**
* PrettyPrinter constructor
* @param indentationSize the size (in spaces) of a single indentation
* @param startingIndent the initial number of indentations applied
*/
public PrettyPrinter(let indentationSize!: UInt64 = 4, let startingIndent!: UInt64 = 0) {
currentIndent = " " * Int64(indentationSize) * Int64(startingIndent)
}
/*
* Implement the low-level string printing
* @param s the String to print
*/
protected func put(s: String): Unit
/*
* Implement the low-level newline printing, defaults to put("\n")
*/
protected open func putNewLine(): Unit {
put("\n")
}
/*
* Implement the low-level color setting, if this printer does not support colors,
* does nothing
* @param color the color to set
*/
protected func setColor(color: Color): Unit
/**
* current indentation string
*/
private var currentIndent: String
/**
* flag that is set up every time an indentation is needed
*/
private var needIndent = false
/**
* current color, used to track color switches
* Note: this field has no relation to setColor function
*/
private var currentColor: Color = DEFAULT_COLOR
/**
* Is this pretty-printer on the top level of indentation?
* @return false if current indent is not empty
*/
public prop isTopLevel: Bool {
get() { currentIndent.isEmpty() }
}
/**
* Run a block of code with indentation
* Typical usage:
pp.indent {
pp.appendLine("1")
pp.appendLine("2")
pp.appendLine("3")
}
* This outputs three lines with "1", "2" and "3", all indented one indentation
* level more than current indentation
* @param body the code block to run with indentation
*/
public func indent(body: () -> Unit): PrettyPrinter {
indent(1, body)
}
/**
* Run a block of code with indentation
* Typical usage:
pp.indent(2) {
pp.appendLine("1")
pp.appendLine("2")
pp.appendLine("3")
}
* This outputs three lines with "1", "2" and "3", all indented 2 indentation
* levels more than current indentation
* @param symbols the number of indentations to indent
* @param body the code block to run with indentation
*/
public func indent(indents: UInt64, body: () -> Unit): PrettyPrinter {
customOffset(indents * indentationSize, body)
}
/**
* Run a block of code with fully custom indentation in addition to the current level
* Typical usage:
pp.customOffset(5) {
pp.appendLine("1")
pp.appendLine("2")
pp.appendLine("3")
}
* This outputs three lines with "1", "2" and "3", all indented by exactly 5 spaces
* more than current indentation
*
* This should only be used when fully custom (not representable in indentation offsets)
* offset is needed, in all other cases use indent()
* @param symbols the number of spaces to indent
* @param body the code block to run with indentation
*/
public func customOffset(symbols: UInt64, body: () -> Unit): PrettyPrinter {
let savedIndent = currentIndent
currentIndent += " " * Int64(symbols)
try {
body()
} finally {
currentIndent = savedIndent
}
return this
}
/**
* Run a block of code with a different color
* Typical usage:
pp.colored(RED) {
pp.appendLine("1")
pp.appendLine("2")
pp.appendLine("3")
}
* This outputs three lines with "1", "2" and "3", all in red color
*
* @param color the color to use
* @param body the code block to run with color
*/
public func colored(color: Color, body: () -> Unit): PrettyPrinter {
if (color == currentColor) {
body()
return this
}
let previousColor = currentColor
currentColor = color
setColor(color)
try {
body()
} finally {
currentColor = previousColor
setColor(previousColor)
}
return this
}
/**
* Run a block of code. And if such behavior is supported by corresponding PrettyPrinter
* whatever is printed inside this block of code will be
* fitted into a space that corresponds to a space taken by `spaceSize` halfwidth characters.
*
* @param spaceSize size of the space output must fit into
* @param body the code block to run with color
*/
public open func fillLimitedSpace(spaceSize: Int64, body: () -> Unit): PrettyPrinter {
body()
return this
}
/**
* Print a string with a different color
* @param color the color to use
* @param string the string to print
*/
public func colored(color: Color, text: String): PrettyPrinter {
if (currentColor == color) {
return append(text)
}
let previousColor = currentColor
currentColor = color
setColor(color)
append(text)
currentColor = previousColor
setColor(previousColor)
return this
}
/**
* Append a string to this pretty printer
* Note: this does not support multiline strings, no indentation is provided for that case
* @param value the string to print
*/
public func append(text: String): PrettyPrinter {
if (needIndent) {
put(currentIndent)
needIndent = false
}
put(text)
return this
}
/**
* Append a string to this pretty printer, aligned in the space of `size` characters, centered
* Note: this does not support multiline strings, no indentation is provided for that case
* @param assert the string to print
* @param size the size of the box to use
*/
public func appendCentered(text: String, space: UInt64): PrettyPrinter {
let actualSize = UInt64(text.size)
let leftSpaces = (space - actualSize) / 2
appendPadded(text, leftSpaces, space)
}
public func appendLeftAligned(text: String, space: UInt64): PrettyPrinter {
appendPadded(text, 0, space)
}
public func appendRightAligned(text: String, space: UInt64): PrettyPrinter {
let actualSize = UInt64(text.size)
let dd = space - actualSize
appendPadded(text, dd, space)
}
private func appendPadded(text: String, leftSpaces: UInt64, space: UInt64): PrettyPrinter {
if (UInt64(text.size) >= space) {
return append(text)
}
let rightSpaces = space - leftSpaces - UInt64(text.size)
append(" " * Int64(leftSpaces))
append(text)
append(" " * Int64(rightSpaces))
}
/**
* Append a PrettyPrintable value to this pretty printer
* @param value the value to print
*/
public func append<PP>(value: PP): PrettyPrinter where PP <: PrettyPrintable {
if (needIndent) {
put(currentIndent)
needIndent = false
}
value.pprint(this)
return this
}
/**
* Print a newline
*/
public func newLine(): PrettyPrinter {
putNewLine()
needIndent = true
return this
}
/**
* Print a given string, followed by a newline
* @param value the string to print
*/
public func appendLine(text: String): PrettyPrinter {
append(text)
newLine()
return this
}
/**
* Print a given PrettyPrintable value, followed by a newline
* @param value the value to print
*/
public func appendLine<PP>(value: PP): PrettyPrinter where PP <: PrettyPrintable {
append(value)
newLine()
return this
}
}
/**
* Internal enum used to implement PrettyText, should not be used directly
* A chunk of output produced by pretty printer:
* - A string fragment
* - A color switch
* - A newline
*/
enum PrettyPrintableTextChunk {
| PPTextPiece(String)
| PPColorSwitch(Color)
| PPNewLine
}
/**
* A builder-like class to store pretty-printed output.
* Main usage is for intermediate storage and passing of such values.
* Implements both PrettyPrinter (can be printed to) and PrettyPrintable (can be printed from)
*/
public class PrettyText <: PrettyPrinter & PrettyPrintable & ToString {
PrettyText(let data: ArrayList<PrettyPrintableTextChunk>) {}
/**
* Default constructor: makes an empty PrettyText
*/
public init() {
this(ArrayList())
}
/**
* String-based constructor: makes a PrettyText with starting content of `string`
*/
public init(string: String) {
this()
put(string)
}
/**
* Utility factory function: create a PrettyText from a PrettyPrintable by printing it.
* This is static only because generic constructors are not allowed.
*/
public static func of<PP>(pp: PP): PrettyText where PP <: PrettyPrintable {
let result = PrettyText()
pp.pprint(result)
result
}
/**
* @return true if nothing has been put into this object yet, false otherwise
*/
public func isEmpty(): Bool {
data.isEmpty()
}
protected override func put(s: String): Unit {
if (!s.isEmpty()) {
data.add(PPTextPiece(s))
}
}
protected override func setColor(color: Color): Unit {
data.add(PPColorSwitch(color))
}
protected override func putNewLine(): Unit {
data.add(PPNewLine)
}
/**
* Print this PrettyText into a pretty printer: output should look exactly like input
* @param to the pretty printer to print to
*/
public func pprint(to: PrettyPrinter): PrettyPrinter {
for (element in data) {
match (element) {
case PPTextPiece(s) => to.append(s)
case PPColorSwitch(c) => to.setColor(c)
case PPNewLine => to.newLine()
}
}
return to
}
/**
* Print this PrettyText to a String: output should look exactly like it would on an output printer,
* but without colors
*/
public func toString(): String {
let result = StringBuilder()
for (element in data) {
match (element) {
case PPTextPiece(s) => result.append(s)
case PPColorSwitch(_) => ()
case PPNewLine => result.append("\n")
}
}
return result.toString()
}
}
/**
* Pretty printable: interface signifying this type can be pretty-printed
*/
public interface PrettyPrintable {
/**
* Pretty-print this object to a given printer
* @param to the pretty printer to print to
* @return the same value as to
*/
func pprint(to: PrettyPrinter): PrettyPrinter
}
/**
* Pretty printable instance for Array: print elements in succession
*/
extend<T> Array<T> <: PrettyPrintable where T <: PrettyPrintable {
public func pprint(to: PrettyPrinter): PrettyPrinter {
for (e in this) {
e.pprint(to)
}
return to
}
}
/**
* Pretty printable instance for ArrayList: print elements in succession
*/
extend<T> ArrayList<T> <: PrettyPrintable where T <: PrettyPrintable {
public func pprint(to: PrettyPrinter): PrettyPrinter {
for (e in this) {
e.pprint(to)
}
return to
}
}
protected const NOT_PRINTABLE_PLACEHOLDER = "<value not printable>"
protected func toStringOrPlaceholder<T>(value: T) {
return (value as ToString)?.toString() ?? NOT_PRINTABLE_PLACEHOLDER
}
protected func toStringQuotedOrPlaceholder<T>(value: T) {
return match (value) {
case vStr: String => quoteString(vStr)
case _ => toStringOrPlaceholder(value)
}
}
protected func quoteString<T>(value: T) where T <: ToString {
return "\"${value}\""
}