/*
* 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.enumerate
import std.convert.Formattable
private func isNonPrintable(char: Rune): Bool {
let cp = UInt32(char)
// characters with codepoints 0x00..0x1F are basic ascii control characters (C0)
// 0x7F is DEL, also a control character
// characters with codepoints 0x80..0x9F are extended control characters (C1)
// NOTE: some of the characters handled separately in shieldEscapeSeq
// are also in this ranges, which is by design
if (0x00 <= cp && cp <= 0x1F || 0x7f <= cp && cp <= 0x9f) {
return true
}
// TODO: also escape other characters from category r'C' when/if we have API for that
// currently, regex /\p{C}/ doesn't work properly
return false
}
private func asCodePointEscape(char: Rune): String {
let resultBuilder = StringBuilder("\\u{")
let cp = UInt32(char)
if (cp <= 0xffff) {
resultBuilder.append(cp.format("04x"))
} else {
resultBuilder.append(cp.format("08x"))
}
resultBuilder.append(r'}')
return resultBuilder.toString()
}
/**
* Escape the given string: print non-printable characters like control characters
* as their escaped variants. Also escape any utf-8 sequences longer than 1 byte.
* For example, TAB, CR or LF are printed as r'\t', r'\r' and r'\n' respectively,
* and will be printed as '\u6211'. All one-byte utf-8 characters are left as-is.
* Random-generated data may (and probably will) contain characters that may break the terminal,
* so in order to be safe we try to escape such things when printing this data.
*/
protected func shieldEscapeSeq(
s: String,
shieldSingleQuote!: Bool = true,
shieldDoubleQuote!: Bool = true,
shieldBackslash!: Bool = true,
shieldInterpolation!: Bool = true
): String {
let resultBuilder = StringBuilder()
let runes = s.toRuneArray()
for ((i, ch) in runes |> enumerate) {
match (ch) {
case r'\0' => resultBuilder.append("\\0")
case r'\\' where shieldBackslash => resultBuilder.append("\\\\")
case r'\b' => resultBuilder.append("\\b")
case r'\f' => resultBuilder.append("\\f")
case r'\n' => resultBuilder.append("\\n")
case r'\r' => resultBuilder.append("\\r")
case r'\t' => resultBuilder.append("\\t")
case r'\v' => resultBuilder.append("\\v")
case r'\'' where shieldSingleQuote => resultBuilder.append("\\\'")
case r'\"' where shieldDoubleQuote => resultBuilder.append("\\\"")
case r'\$' where shieldInterpolation && i < runes.size - 1 && runes[i + 1] == r'{' => resultBuilder.append(
"\\\$")
case _ =>
if (isNonPrintable(ch)) {
resultBuilder.append(asCodePointEscape(ch))
} else {
resultBuilder.append(ch)
}
}
}
return resultBuilder.toString()
}
extend PrettyPrinter {
protected func space() {
append(" ")
}
protected func appendLine() {
appendLine("")
}
}