// Port of rich/style.py.
///|
/// A terminal style: a color, a background color and a number of attributes
/// (bold, italic, ...). Attributes have three states: on (`Some(true)`), off
/// (`Some(false)`) or not set (`None`).
pub struct Style {
priv color_ : @color.Color?
priv bgcolor_ : @color.Color?
priv attributes : Int
priv set_attributes : Int
priv link_ : String?
priv link_id_ : String
priv meta_ : Map[String, Json]?
priv null : Bool
/// Color system (`ColorSystem::to_int`) of `ansi_codes`, or -1.
priv mut ansi_system : Int
/// Cached SGR parameters for `ansi_system` (upstream `_ansi`).
priv mut ansi_codes : String
/// Cached hash of the compared fields, 0 when not computed yet.
priv mut hash_ : Int
priv mut definition : String?
}
///|
/// Bit numbers of the style attributes.
let attribute_names : FixedArray[String] = [
"bold", "dim", "italic", "underline", "blink", "blink2", "reverse", "conceal",
"strike", "underline2", "frame", "encircle", "overline",
]
///|
/// SGR parameter for each attribute bit.
let style_map : FixedArray[String] = [
"1", "2", "3", "4", "5", "6", "7", "8", "9", "21", "51", "52", "53",
]
///|
/// Style attribute names accepted by `Style::parse`, and the bit they map to.
let style_attributes : Map[String, Int] = {
"dim": 1,
"d": 1,
"bold": 0,
"b": 0,
"italic": 2,
"i": 2,
"underline": 3,
"u": 3,
"blink": 4,
"blink2": 5,
"reverse": 6,
"r": 6,
"conceal": 7,
"c": 7,
"strike": 8,
"s": 8,
"underline2": 9,
"uu": 9,
"frame": 10,
"encircle": 11,
"overline": 12,
"o": 12,
}
///|
let id_counter : Ref[Int] = Ref(initial_link_id())
///|
fn initial_link_id() -> Int {
// upstream starts from getrandbits(24)
(@env.now() % 16777216UL).to_int()
}
///|
fn next_link_id() -> Int {
let id = id_counter.val
id_counter.val = id + 1
id
}
///|
fn meta_hash(meta : Map[String, Json]?) -> String {
match meta {
None => ""
Some(m) => {
let h = Json::object(m).stringify().hash()
h.to_string()
}
}
}
///|
/// Canonical text of the meta data (upstream compares pickled bytes, so key
/// order matters).
fn meta_key(meta : Map[String, Json]?) -> String {
match meta {
None => ""
Some(m) => Json::object(m).stringify()
}
}
///|
/// Deep copy JSON (objects and arrays are mutable).
fn json_deep_copy(j : Json) -> Json {
match j {
Array(items) => Json::array(items.map(json_deep_copy))
Object(m) => {
let copy : Map[String, Json] = Map([])
for k, v in m {
copy[k] = json_deep_copy(v)
}
Json::object(copy)
}
_ => j
}
}
///|
fn meta_deep_copy(meta : Map[String, Json]?) -> Map[String, Json]? {
match meta {
None => None
Some(m) => {
let copy : Map[String, Json] = Map([])
for k, v in m {
copy[k] = json_deep_copy(v)
}
Some(copy)
}
}
}
///|
fn non_empty(s : String?) -> String? {
match s {
Some("") | None => None
s => s
}
}
///|
fn meta_truthy(meta : Map[String, Json]?) -> Bool {
meta is Some(m) && !m.is_empty()
}
///|
fn make_style(
color~ : @color.Color?,
bgcolor~ : @color.Color?,
attributes~ : Int,
set_attributes~ : Int,
link~ : String?,
link_id~ : String,
meta~ : Map[String, Json]?,
null~ : Bool,
definition? : String? = None,
) -> Style {
{
color_: color,
bgcolor_: bgcolor,
attributes,
set_attributes,
link_: link,
link_id_: link_id,
meta_: meta,
null,
ansi_system: -1,
ansi_codes: "",
hash_: 0,
definition,
}
}
///|
/// Create a new style.
pub fn Style::new(
color? : @color.Color,
bgcolor? : @color.Color,
bold? : Bool,
dim? : Bool,
italic? : Bool,
underline? : Bool,
blink? : Bool,
blink2? : Bool,
reverse? : Bool,
conceal? : Bool,
strike? : Bool,
underline2? : Bool,
frame? : Bool,
encircle? : Bool,
overline? : Bool,
link? : String,
meta? : Map[String, Json],
) -> Style {
let meta = meta_deep_copy(meta)
let values = [
bold, dim, italic, underline, blink, blink2, reverse, conceal, strike, underline2,
frame, encircle, overline,
]
let mut set_attributes = 0
let mut attributes = 0
for bit, v in values {
if v is Some(on) {
set_attributes = set_attributes | (1 << bit)
if on {
attributes = attributes | (1 << bit)
}
}
}
let link_id = if non_empty(link) is Some(_) || meta_truthy(meta) {
"\{next_link_id()}\{meta_hash(meta)}"
} else {
""
}
let null = !(set_attributes != 0 ||
color is Some(_) ||
bgcolor is Some(_) ||
non_empty(link) is Some(_) ||
meta_truthy(meta))
make_style(
color~,
bgcolor~,
attributes~,
set_attributes~,
link~,
link_id~,
meta~,
null~,
)
}
///|
let null_style : Style = Style::new()
///|
/// The "null" style, equivalent to `Style::new()`.
pub fn Style::null() -> Style {
null_style
}
///|
/// Create a new style with colors and no attributes.
pub fn Style::from_color(
color? : @color.Color,
bgcolor? : @color.Color,
) -> Style {
make_style(
color~,
bgcolor~,
attributes=0,
set_attributes=0,
link=None,
link_id="",
meta=None,
null=!(color is Some(_) || bgcolor is Some(_)),
)
}
///|
/// Create a new style with meta data.
pub fn Style::from_meta(meta : Map[String, Json]?) -> Style {
let meta = meta_deep_copy(meta)
make_style(
color=None,
bgcolor=None,
attributes=0,
set_attributes=0,
link=None,
link_id="\{next_link_id()}\{meta_hash(meta)}",
meta~,
null=!meta_truthy(meta),
)
}
///|
/// Create a blank style with meta information; each handler `key` is stored
/// as `"@key"`.
pub fn Style::on(
meta? : Map[String, Json],
handlers? : Map[String, Json],
) -> Style {
let m : Map[String, Json] = match meta {
Some(m) => m.copy()
None => Map([])
}
if handlers is Some(h) {
for k, v in h {
m["@" + k] = v
}
}
Style::from_meta(Some(m))
}
///|
fn Style::bit(self : Style, bit : Int) -> Bool? {
if (self.set_attributes & (1 << bit)) != 0 {
Some((self.attributes & (1 << bit)) != 0)
} else {
None
}
}
///|
pub fn Style::bold(self : Style) -> Bool? {
self.bit(0)
}
///|
pub fn Style::dim(self : Style) -> Bool? {
self.bit(1)
}
///|
pub fn Style::italic(self : Style) -> Bool? {
self.bit(2)
}
///|
pub fn Style::underline(self : Style) -> Bool? {
self.bit(3)
}
///|
pub fn Style::blink(self : Style) -> Bool? {
self.bit(4)
}
///|
pub fn Style::blink2(self : Style) -> Bool? {
self.bit(5)
}
///|
pub fn Style::reverse(self : Style) -> Bool? {
self.bit(6)
}
///|
pub fn Style::conceal(self : Style) -> Bool? {
self.bit(7)
}
///|
pub fn Style::strike(self : Style) -> Bool? {
self.bit(8)
}
///|
pub fn Style::underline2(self : Style) -> Bool? {
self.bit(9)
}
///|
pub fn Style::frame(self : Style) -> Bool? {
self.bit(10)
}
///|
pub fn Style::encircle(self : Style) -> Bool? {
self.bit(11)
}
///|
pub fn Style::overline(self : Style) -> Bool? {
self.bit(12)
}
///|
/// The foreground color, if set.
pub fn Style::color(self : Style) -> @color.Color? {
self.color_
}
///|
/// The background color, if set.
pub fn Style::bgcolor(self : Style) -> @color.Color? {
self.bgcolor_
}
///|
/// Link URL, if set.
pub fn Style::link(self : Style) -> String? {
self.link_
}
///|
/// The link id, used in the ANSI code for links.
pub fn Style::link_id(self : Style) -> String {
self.link_id_
}
///|
/// Meta information (empty if not set).
pub fn Style::meta(self : Style) -> Map[String, Json] {
match meta_deep_copy(self.meta_) {
Some(m) => m
None => Map([])
}
}
///|
/// A style is "null" if it has no attributes, colors, links or meta. This is
/// the negation of upstream's `bool(style)`.
pub fn Style::is_null(self : Style) -> Bool {
self.null
}
///|
/// No link and no meta: combining such styles gives a style that only
/// depends on the inputs' compared fields.
fn Style::is_plain(self : Style) -> Bool {
self.meta_ is None && non_empty(self.link_) is None && self.link_id_ == ""
}
///|
/// Upstream `bool(style)`.
pub fn Style::to_bool(self : Style) -> Bool {
!self.null
}
///|
/// Check if the style specified a transparent background.
pub fn Style::transparent_background(self : Style) -> Bool {
match self.bgcolor_ {
None => true
Some(c) => c.is_default()
}
}
///|
/// A Style with background only.
pub fn Style::background_style(self : Style) -> Style {
Style::new(bgcolor?=self.bgcolor_)
}
///|
/// Re-generate the style definition from attributes (upstream `str(style)`).
pub fn Style::to_definition(self : Style) -> String {
if self.definition is Some(d) {
return d
}
let attributes = []
let bits = self.set_attributes
for bit in 0..<13 {
if (bits & (1 << bit)) != 0 {
let name = attribute_names[bit]
if (self.attributes & (1 << bit)) != 0 {
attributes.push(name)
} else {
attributes.push("not " + name)
}
}
}
if self.color_ is Some(c) {
attributes.push(c.name)
}
if self.bgcolor_ is Some(c) {
attributes.push("on")
attributes.push(c.name)
}
if non_empty(self.link_) is Some(link) {
attributes.push("link")
attributes.push(link)
}
let definition = if attributes.is_empty() {
"none"
} else {
attributes.join(" ")
}
self.definition = Some(definition)
definition
}
///|
/// `Show` gives the style definition, like upstream `str(style)`.
pub impl Show for Style with fn output(self, logger) {
logger.write_string(self.to_definition())
}
///|
/// Upstream `repr(style)`, e.g. `Style(color=Color('red', ...), bold=True)`.
pub fn Style::repr(self : Style) -> String {
let parts = []
if self.color_ is Some(c) {
parts.push("color=\{c}")
}
if self.bgcolor_ is Some(c) {
parts.push("bgcolor=\{c}")
}
let order = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
for bit in order {
if self.bit(bit) is Some(v) {
parts.push("\{attribute_names[bit]}=\{py_bool(v)}")
}
}
if self.link_ is Some(link) {
parts.push("link=\{py_repr(link)}")
}
if meta_truthy(self.meta_) {
parts.push("meta=\{py_json_repr(Json::object(self.meta_.unwrap()))}")
}
"Style(" + parts.join(", ") + ")"
}
///|
fn color_key(c : @color.Color?) -> Int {
match c {
None => 0
Some(c) => c.hash()
}
}
///|
pub impl Eq for Style with fn equal(self, other) {
physical_equal(self, other) ||
(
self.attributes == other.attributes &&
self.set_attributes == other.set_attributes &&
self.color_ == other.color_ &&
self.bgcolor_ == other.bgcolor_ &&
self.link_ == other.link_ &&
meta_key(self.meta_) == meta_key(other.meta_)
)
}
///|
pub impl Hash for Style with fn hash_combine(self, hasher) {
hasher.combine(self.hash_value())
}
///|
/// Hash of the fields compared by `Eq`, computed once (styles are immutable).
fn Style::hash_value(self : Style) -> Int {
if self.hash_ != 0 {
return self.hash_
}
let hasher = Hasher()
hasher.combine(color_key(self.color_))
hasher.combine(color_key(self.bgcolor_))
hasher.combine(self.attributes)
hasher.combine(self.set_attributes)
hasher.combine(self.link_.unwrap_or(""))
hasher.combine(meta_key(self.meta_))
let h = hasher.finalize()
let h = if h == 0 { 1 } else { h }
self.hash_ = h
h
}
///|
/// Generate ANSI codes for this style.
fn Style::make_ansi_codes(
self : Style,
color_system : @color.ColorSystem,
) -> String {
let key = color_system.to_int()
if self.ansi_system == key {
return self.ansi_codes
}
// upstream joins the SGR parameters with ";": written directly here
let buf = StringBuilder()
let attributes = self.attributes & self.set_attributes
if attributes != 0 {
for bit in 0..<13 {
if (attributes & (1 << bit)) != 0 {
if !buf.is_empty() {
buf.write_char(';')
}
buf.write_string(style_map[bit])
}
}
}
if self.color_ is Some(c) {
write_color_codes(buf, c.downgrade(color_system), true)
}
if self.bgcolor_ is Some(c) {
write_color_codes(buf, c.downgrade(color_system), false)
}
let codes = buf.to_string()
self.ansi_system = key
self.ansi_codes = codes
codes
}
///|
/// Append `color.get_ansi_codes(foreground~)` joined with ";" (after a ";"
/// when `buf` is not empty).
fn write_color_codes(
buf : StringBuilder,
color : @color.Color,
foreground : Bool,
) -> Unit {
if !buf.is_empty() {
buf.write_char(';')
}
match color.type_ {
Default => buf.write_string(if foreground { "39" } else { "49" })
Windows | Standard => {
let number = color.number.unwrap()
let (fore, back) = if number < 8 { (30, 40) } else { (82, 92) }
write_decimal(buf, if foreground { fore + number } else { back + number })
}
EightBit => {
buf.write_string(if foreground { "38;5;" } else { "48;5;" })
write_decimal(buf, color.number.unwrap())
}
Truecolor => {
let t = color.triplet.unwrap()
buf.write_string(if foreground { "38;2;" } else { "48;2;" })
write_decimal(buf, t.red)
buf.write_char(';')
write_decimal(buf, t.green)
buf.write_char(';')
write_decimal(buf, t.blue)
}
}
}
///|
/// `buf.write_string(n.to_string())` without the temporary string for the
/// usual 0..=999.
fn write_decimal(buf : StringBuilder, n : Int) -> Unit {
if n < 0 || n > 999 {
buf.write_string(n.to_string())
return
}
if n >= 100 {
buf.write_char(Int::unsafe_to_char('0'.to_int() + n / 100))
}
if n >= 10 {
buf.write_char(Int::unsafe_to_char('0'.to_int() + n / 10 % 10))
}
buf.write_char(Int::unsafe_to_char('0'.to_int() + n % 10))
}
///|
let normalize_cache : @memo.Memo[String, String] = @memo.Memo::new(1024)
///|
/// Normalize a style definition so that styles with the same effect have
/// the same string representation.
pub fn Style::normalize(style : String) -> String {
if normalize_cache.get(style) is Some(s) {
return s
}
let result = Style::parse(style).to_definition() catch {
_ => @pystr.lower(py_strip(style))
}
normalize_cache.set(style, result)
result
}
///|
let parse_cache : @memo.Memo[String, Style] = @memo.Memo::new(4096)
///|
fn split_words(s : String) -> Array[String] {
let words = []
for_each_word(s, (start, end) => words.push(s.unsafe_substring(start~, end~)))
words
}
///|
/// Call `f(start, end)` for each maximal run of non-whitespace code units
/// (Python `str.split()`; whitespace characters are all in the BMP, so
/// scanning UTF-16 code units is exact).
fn for_each_word(s : String, f : (Int, Int) -> Unit) -> Unit {
let n = s.length()
let mut i = 0
while i < n {
while i < n && is_py_space_unit(s.unsafe_get(i).to_int()) {
i += 1
}
let start = i
while i < n && !is_py_space_unit(s.unsafe_get(i).to_int()) {
i += 1
}
if i > start {
f(start, i)
}
}
}
///|
/// `is_py_whitespace` for a UTF-16 code unit (surrogates are not spaces).
fn is_py_space_unit(u : Int) -> Bool {
if u < 0x80 {
u == 0x20 || (u >= 0x09 && u <= 0x0d) || (u >= 0x1c && u <= 0x1f)
} else {
is_py_whitespace(u.unsafe_to_char())
}
}
///|
/// Python `str.isspace()` for a single character.
fn is_py_whitespace(c : Char) -> Bool {
match c {
' ' | '\t' | '\n' | '\r' | '\u{0b}' | '\u{0c}' => true
'\u{1c}' | '\u{1d}' | '\u{1e}' | '\u{1f}' | '\u{85}' | '\u{a0}' => true
'\u{1680}'
| '\u{2028}'
| '\u{2029}'
| '\u{202f}'
| '\u{205f}'
| '\u{3000}' => true
_ => c.to_int() >= 0x2000 && c.to_int() <= 0x200a
}
}
///|
/// Python `str.strip()`.
fn py_strip(s : String) -> String {
let mut start = 0
let mut end = s.length()
while start < end && is_py_space_unit(s.unsafe_get(start).to_int()) {
start += 1
}
while end > start && is_py_space_unit(s.unsafe_get(end - 1).to_int()) {
end -= 1
}
if start == 0 && end == s.length() {
s
} else {
s.unsafe_substring(start~, end~)
}
}
///|
/// Parse a style definition such as `"bold red on white"`.
pub fn Style::parse(style_definition : String) -> Style raise RichError {
if parse_cache.get(style_definition) is Some(s) {
return s
}
let style = parse_style(style_definition)
parse_cache.set(style_definition, style)
style
}
///|
fn parse_style(style_definition : String) -> Style raise RichError {
if py_strip(style_definition) == "none" || style_definition == "" {
return Style::null()
}
let mut color : @color.Color? = None
let mut bgcolor : @color.Color? = None
let mut attributes = 0
let mut set_attributes = 0
let mut link : String? = None
let words = split_words(style_definition)
let mut i = 0
fn next_word() -> String {
if i < words.length() {
let w = words[i]
i += 1
w
} else {
""
}
}
while i < words.length() {
let original_word = next_word()
let word = @pystr.lower(original_word)
if word == "on" {
let word = next_word()
if word == "" {
raise StyleSyntaxError("color expected after 'on'")
}
let c = @color.Color::parse(word) catch {
e =>
raise StyleSyntaxError(
"unable to parse \{py_repr(word)} as background color; \{e}",
)
}
bgcolor = Some(c)
} else if word == "not" {
let word = next_word()
match style_attributes.get(word) {
None =>
raise StyleSyntaxError(
"expected style attribute after 'not', found \{py_repr(word)}",
)
Some(bit) => {
set_attributes = set_attributes | (1 << bit)
attributes = attributes & (1 << bit).lnot()
}
}
} else if word == "link" {
let word = next_word()
if word == "" {
raise StyleSyntaxError("URL expected after 'link'")
}
link = Some(word)
} else if style_attributes.get(word) is Some(bit) {
set_attributes = set_attributes | (1 << bit)
attributes = attributes | (1 << bit)
} else {
let c = @color.Color::parse(word) catch {
e =>
raise StyleSyntaxError(
"unable to parse \{py_repr(word)} as color; \{e}",
)
}
color = Some(c)
}
}
let link_id = if non_empty(link) is Some(_) {
"\{next_link_id()}"
} else {
""
}
make_style(
color~,
bgcolor~,
attributes~,
set_attributes~,
link~,
link_id~,
meta=None,
null=!(set_attributes != 0 ||
color is Some(_) ||
bgcolor is Some(_) ||
non_empty(link) is Some(_)),
)
}
///|
/// Get a CSS style rule.
pub fn Style::get_html_style(
self : Style,
theme? : @color.TerminalTheme,
) -> String {
let theme = theme.unwrap_or(@color.default_terminal_theme)
let css = []
let mut color = self.color_
let mut bgcolor = self.bgcolor_
if self.reverse() is Some(true) {
let tmp = color
color = bgcolor
bgcolor = tmp
}
if self.dim() is Some(true) {
let foreground_color = match color {
None => theme.foreground_color
Some(c) => c.get_truecolor(theme~)
}
color = Some(
@color.Color::from_triplet(
@color.blend_rgb(
foreground_color,
theme.background_color,
cross_fade=0.5,
),
),
)
}
if color is Some(c) {
let theme_color = c.get_truecolor(theme~)
css.push("color: \{theme_color.hex()}")
css.push("text-decoration-color: \{theme_color.hex()}")
}
if bgcolor is Some(c) {
let theme_color = c.get_truecolor(theme~, foreground=false)
css.push("background-color: \{theme_color.hex()}")
}
if self.bold() is Some(true) {
css.push("font-weight: bold")
}
if self.italic() is Some(true) {
css.push("font-style: italic")
}
if self.underline() is Some(true) {
css.push("text-decoration: underline")
}
if self.strike() is Some(true) {
css.push("text-decoration: line-through")
}
if self.overline() is Some(true) {
css.push("text-decoration: overline")
}
css.join("; ")
}
///|
/// Combine styles from left to right.
pub fn Style::combine(styles : ArrayView[Style]) -> Style {
guard styles is [first, .. rest] else { abort("expected at least one style") }
let mut result = first
for s in rest {
result = result + s
}
result
}
///|
/// Combine styles from left to right (upstream `Style.chain(*styles)`).
pub fn Style::chain(styles : Array[Style]) -> Style {
Style::combine(styles)
}
///|
/// Get a copy of this style (links get a new link id).
pub fn Style::copy(self : Style) -> Style {
if self.null {
return null_style
}
let style = make_style(
color=self.color_,
bgcolor=self.bgcolor_,
attributes=self.attributes,
set_attributes=self.set_attributes,
link=self.link_,
link_id=if non_empty(self.link_) is Some(_) {
"\{next_link_id()}"
} else {
""
},
meta=self.meta_,
null=false,
definition=self.definition,
)
style.ansi_system = self.ansi_system
style.ansi_codes = self.ansi_codes
style
}
///|
/// `copy()` for internal callers that do not depend on the copy being a
/// distinct object: styles are immutable, so without a link (id) the copy
/// would be indistinguishable from `self`.
fn Style::copy_value(self : Style) -> Style {
if self.link_id_ == "" && non_empty(self.link_) is None {
self
} else {
self.copy()
}
}
///|
/// Get a copy of the style with color removed.
pub fn Style::without_color(self : Style) -> Style {
if self.null {
return null_style
}
make_style(
color=None,
bgcolor=None,
attributes=self.attributes,
set_attributes=self.set_attributes,
link=self.link_,
link_id=if non_empty(self.link_) is Some(_) {
"\{next_link_id()}"
} else {
""
},
meta=None,
null=false,
)
}
///|
/// Get a copy of this style with link and meta information removed.
pub fn Style::clear_meta_and_links(self : Style) -> Style {
if self.null {
return null_style
}
let style = make_style(
color=self.color_,
bgcolor=self.bgcolor_,
attributes=self.attributes,
set_attributes=self.set_attributes,
link=None,
link_id="",
meta=None,
null=false,
// the cached definition mentions the link: do not reuse it (upstream does)
)
style.ansi_system = self.ansi_system
style.ansi_codes = self.ansi_codes
style
}
///|
/// Get a copy with a different value for link.
pub fn Style::update_link(self : Style, link : String?) -> Style {
let style = make_style(
color=self.color_,
bgcolor=self.bgcolor_,
attributes=self.attributes,
set_attributes=self.set_attributes,
link~,
link_id=if non_empty(link) is Some(_) { "\{next_link_id()}" } else { "" },
meta=self.meta_,
null=false,
// the cached definition mentions the link: do not reuse it (upstream does)
)
style.ansi_system = self.ansi_system
style.ansi_codes = self.ansi_codes
style
}
///|
/// Render the ANSI codes for the style around `text`. With `color_system`
/// `None` the text is returned unstyled.
pub fn Style::render(
self : Style,
text? : String = "",
color_system? : @color.ColorSystem? = Some(Truecolor),
legacy_windows? : Bool = false,
) -> String {
guard text != "" && color_system is Some(color_system) else { return text }
let attrs = self.make_ansi_codes(color_system)
let rendered = if attrs != "" {
"\u{1b}[\{attrs}m\{text}\u{1b}[0m"
} else {
text
}
match non_empty(self.link_) {
Some(link) if !legacy_windows =>
"\u{1b}]8;id=\{self.link_id_};\{link}\u{1b}\\\{rendered}\u{1b}]8;;\u{1b}\\"
_ => rendered
}
}
///|
/// `render` writing into `out` (the console's output buffer).
fn Style::render_to(
self : Style,
out : StringBuilder,
text : String,
color_system : @color.ColorSystem?,
legacy_windows : Bool,
) -> Unit {
guard text != "" && color_system is Some(color_system) else {
out.write_string(text)
return
}
let attrs = self.make_ansi_codes(color_system)
let link = match non_empty(self.link_) {
Some(link) if !legacy_windows => {
out.write_string("\u{1b}]8;id=")
out.write_string(self.link_id_)
out.write_char(';')
out.write_string(link)
out.write_string("\u{1b}\\")
true
}
_ => false
}
if attrs != "" {
out.write_string("\u{1b}[")
out.write_string(attrs)
out.write_char('m')
out.write_string(text)
out.write_string("\u{1b}[0m")
} else {
out.write_string(text)
}
if link {
out.write_string("\u{1b}]8;;\u{1b}\\")
}
}
///|
fn Style::add_style(self : Style, style : Style) -> Style {
if style.null {
return self
}
if self.null {
return style
}
// Without meta the result only depends on the fields compared by `Eq`
// (and is copied by `+` when it has a link), so it can be memoized like
// upstream's `lru_cache`d `_add`.
if self.meta_ is None && style.meta_ is None {
let key = (self, style)
match add_cache.get(key) {
Some(combined) => return combined
None => {
let combined = self.add_style_uncached(style)
add_cache.set(key, combined)
return combined
}
}
}
self.add_style_uncached(style)
}
///|
/// Memo table of `Style::add_style` (upstream `lru_cache(maxsize=1024)`).
let add_cache : @memo.Memo[(Style, Style), Style] = @memo.Memo::new(1024)
///|
fn Style::add_style_uncached(self : Style, style : Style) -> Style {
let meta = if meta_truthy(self.meta_) && meta_truthy(style.meta_) {
let m = self.meta_.unwrap().copy()
for k, v in style.meta_.unwrap() {
m[k] = v
}
Some(m)
} else if meta_truthy(self.meta_) {
self.meta_
} else if meta_truthy(style.meta_) {
style.meta_
} else {
// `self._meta or style._meta`: an empty meta is still a value
match self.meta_ {
Some(m) if !m.is_empty() => self.meta_
_ => style.meta_
}
}
make_style(
color=match style.color_ {
Some(_) => style.color_
None => self.color_
},
bgcolor=match style.bgcolor_ {
Some(_) => style.bgcolor_
None => self.bgcolor_
},
attributes=(self.attributes & style.set_attributes.lnot()) |
(style.attributes & style.set_attributes),
set_attributes=self.set_attributes | style.set_attributes,
link=match non_empty(style.link_) {
Some(_) => style.link_
None => self.link_
},
link_id=if style.link_id_ != "" { style.link_id_ } else { self.link_id_ },
meta~,
null=false,
)
}
///|
/// Combine two styles; attributes of `other` take precedence.
pub impl Add for Style with fn add(self, other) {
let combined = self.add_style(other)
if non_empty(combined.link_) is Some(_) {
combined.copy()
} else {
combined
}
}
///|
/// Combine with an optional style (upstream `style + None`).
pub fn Style::add_opt(self : Style, other : Style?) -> Style {
match other {
None => self
Some(other) => self + other
}
}
///|
/// Pick the first non-`None` style (upstream `Style.pick_first`).
pub fn Style::pick_first(
values : ArrayView[StyleType?],
) -> StyleType raise RichError {
for value in values {
if value is Some(v) {
return v
}
}
raise ValueError("expected at least one non-None style")
}
///|
/// Upstream `repr(stack)`.
pub fn StyleStack::repr(self : StyleStack) -> String {
" s.repr()).join(", ") + "]>"
}
///|
/// A stack of styles.
pub struct StyleStack {
priv stack : Array[Style]
}
///|
pub fn StyleStack::new(default_style : Style) -> StyleStack {
{ stack: [default_style], }
}
///|
/// The Style at the top of the stack.
pub fn StyleStack::current(self : StyleStack) -> Style {
self.stack[self.stack.length() - 1]
}
///|
/// Push a new style on to the stack, combined with the current style.
pub fn StyleStack::push(self : StyleStack, style : Style) -> Unit {
self.stack.push(self.current() + style)
}
///|
/// Pop the last style and return the new current style.
pub fn StyleStack::pop(self : StyleStack) -> Style {
ignore(self.stack.pop())
self.current()
}