// Port of rich/theme.py, rich/themes.py and rich/default_styles.py.
///|
/// The default styles, by name.
pub fn default_styles() -> Map[String, Style] {
let m : Map[String, Style] = Map([])
for item in default_style_definitions {
m[item.0] = Style::parse(item.1) catch { _ => abort("bad default style") }
}
m
}
///|
let default_styles_map : Map[String, Style] = default_styles()
///|
/// A container for style information, used by `Console`.
pub struct Theme {
styles : Map[String, Style]
}
///|
/// Create a theme. With `inherit`, the default styles are included.
/// Style definitions are parsed (raising on syntax errors).
pub fn Theme::new(
styles? : Map[String, &IntoStyle],
inherit_styles? : Bool = true,
) -> Theme raise RichError {
let m : Map[String, Style] = if inherit_styles {
default_styles_map.copy()
} else {
Map([])
}
if styles is Some(styles) {
for name, style in styles {
m[name] = match style.into_style_type() {
Style(s) => s
Def(d) => Style::parse(d)
}
}
}
{ styles: m, }
}
///|
/// Contents of a config file for this theme.
pub fn Theme::config(self : Theme) -> String {
let items = self.styles.to_array()
items.sort_by((a, b) => a.0.compare(b.0))
"[styles]\n" + items.map(kv => "\{kv.0} = \{kv.1}").join("\n")
}
///|
/// Load a theme from the contents of an INI-style config file with a
/// `[styles]` section (upstream `Theme.from_file`, which reads the file
/// with Python's `configparser`: keys are lower-cased, `[DEFAULT]` values
/// are inherited, values may continue on indented lines and `%%` /
/// `%(name)s` are interpolated). Configparser errors are `ValueError`s
/// with the upstream message.
pub fn Theme::from_config(
config : String,
inherit_styles? : Bool = true,
) -> Theme raise RichError {
let parsed : Map[String, &IntoStyle] = Map([])
for item in config_items(config, "styles") {
parsed[item.0] = Style::parse(item.1)
}
Theme::new(styles=parsed, inherit_styles~)
}
///|
/// The `(name, value)` items of a section of an INI file, as Python's
/// `ConfigParser().read_string(config); config.items(section)`.
fn config_items(
config : String,
section_name : String,
) -> Array[(String, String)] raise RichError {
// section name -> ordered option names and their value lines
let sections : Map[String, Map[String, Array[String]]] = Map([])
let defaults : Map[String, Array[String]] = Map([])
let errors = []
let mut cursect : Map[String, Array[String]]? = None
let mut sectname = ""
let mut optname : String? = None
let mut indent_level = 0
let lines = config.split("\n").map(l => l.to_owned()).to_array()
// `split` gives a trailing empty line for a final "\n"
if lines.length() > 0 && lines[lines.length() - 1] == "" {
lines.pop() |> ignore
}
for index, line in lines {
let lineno = index + 1
let value = py_strip(line)
if value == "" || value.has_prefix("#") || value.has_prefix(";") {
if value == "" && cursect is Some(sect) && optname is Some(name) {
// empty lines are kept in multi-line values
if sect.get(name) is Some(v) {
v.push("")
}
} else {
indent_level = 0x7fffffff
}
continue
}
let stripped_left = line.trim_start(chars=" \t").to_owned()
let cur_indent_level = line.length() - stripped_left.length()
if cursect is Some(sect) &&
optname is Some(name) &&
cur_indent_level > indent_level {
if sect.get(name) is Some(v) {
v.push(value)
}
continue
}
indent_level = cur_indent_level
// SECTCRE = r"\[(?P.+)\]" (matched at the start)
if value.has_prefix("[") && value.rev_find("]") is Some(close) && close > 1 {
let name = py_slice(value, 1, close)
if name == "DEFAULT" {
cursect = Some(defaults)
} else {
if sections.contains(name) {
raise ValueError(
"While reading from '??>' [line \{pad2(lineno)}]: section \{py_repr(name)} already exists",
)
}
let sect = Map([])
sections[name] = sect
cursect = Some(sect)
}
sectname = name
optname = None
continue
}
guard cursect is Some(sect) else {
raise ValueError(
"File contains no section headers.\nfile: '??>', line: \{lineno}\n\{py_repr(line + "\n")}",
)
}
// OPTCRE = r"(?P