///|
/// Errors raised while building or querying a style.
pub suberror StyleError {
/// A color that `StyleMeta.colorformat` rejects.
WrongColorFormat(String)
/// `style_for_token` on a token type the style does not know (Python's
/// `KeyError`).
UnknownToken(@token.TokenType)
} derive(Debug)
///|
/// The processed definition of one token type (Python's `_styles` list
/// `[color, bold, italic, underline, bgcolor, border, roman, sans, mono]`).
/// Colors are normalised: `""`, a 6-digit hex value without `#`, an ANSI
/// color name, `transparent`, or a CSS `var(...)`/`calc(...)` expression.
pub(all) struct StyleDef {
color : String
bold : Bool
italic : Bool
underline : Bool
bgcolor : String
border : String
roman : Bool
sans : Bool
mono : Bool
} derive(Eq, Debug)
///|
/// Python's `style_for_token` dictionary. `None` stands for Python's `None`;
/// `roman`, `sans` and `mono` are `True` or `None` in Python, `true` or
/// `false` here.
pub(all) struct TokenStyle {
color : String?
bold : Bool
italic : Bool
underline : Bool
bgcolor : String?
border : String?
roman : Bool
sans : Bool
mono : Bool
ansicolor : String?
bgansicolor : String?
} derive(Eq, Debug)
///|
/// A highlighting style: the equivalent of a `pygments.style.Style` class
/// after `StyleMeta` processed it.
pub struct Style {
/// User-friendly style name (`default`, `monokai`, ...).
name : String
aliases : Array[String]
/// Overall background color; `None` means transparent.
background_color : String?
/// Background color of highlighted lines.
highlight_color : String?
line_number_color : String
line_number_background_color : String
line_number_special_color : String
line_number_special_background_color : String
/// Hidden from the style gallery (language-specific styles).
web_style_gallery_exclude : Bool
/// The raw style strings, one per token type (Python's `styles`, after the
/// metaclass added an empty entry for every standard token type).
styles : Map[@token.TokenType, String]
priv defs : Map[@token.TokenType, StyleDef]
}
///|
let empty_def : StyleDef = {
color: "",
bold: false,
italic: false,
underline: false,
bgcolor: "",
border: "",
roman: false,
sans: false,
mono: false,
}
///|
/// Python's `StyleMeta.colorformat`.
pub fn colorformat(text : String) -> String raise StyleError {
if is_ansicolor(text) {
return text
}
if text.has_prefix("#") {
let col = text.unsafe_substring(start=1, end=text.length())
if col.length() == 6 {
return col
}
if col.length() == 3 {
let buf = StringBuilder()
for c in col {
buf.write_char(c)
buf.write_char(c)
}
return buf.to_string()
}
} else if text == "" || text == "transparent" {
return text
} else if text.has_prefix("var") || text.has_prefix("calc") {
return text
}
raise WrongColorFormat(text)
}
///|
/// Applies the words of a style string to `ndef` (the body of the
/// `StyleMeta` loop).
fn apply_styledefs(
ndef : StyleDef,
words : Array[String],
) -> StyleDef raise StyleError {
let mut d = ndef
for w in words {
match w {
"noinherit" => ()
"bold" => d = { ..d, bold: true, }
"nobold" => d = { ..d, bold: false, }
"italic" => d = { ..d, italic: true, }
"noitalic" => d = { ..d, italic: false, }
"underline" => d = { ..d, underline: true, }
"nounderline" => d = { ..d, underline: false, }
"roman" => d = { ..d, roman: true, }
"sans" => d = { ..d, sans: true, }
"mono" => d = { ..d, mono: true, }
_ =>
if w.has_prefix("bg:") {
d = {
..d,
bgcolor: colorformat(w.unsafe_substring(start=3, end=w.length())),
}
} else if w.has_prefix("border:") {
d = {
..d,
border: colorformat(w.unsafe_substring(start=7, end=w.length())),
}
} else {
d = { ..d, color: colorformat(w), }
}
}
}
d
}
///|
/// Python's `str.split()` (whitespace separated words) for style strings.
fn words(s : String) -> Array[String] {
let out = []
let buf = StringBuilder()
for c in s {
match c {
' ' | '\t' | '\n' | '\r' | '\u{0b}' | '\u{0c}' =>
if buf.to_string() != "" {
out.push(buf.to_string())
buf.reset()
}
_ => buf.write_char(c)
}
}
if buf.to_string() != "" {
out.push(buf.to_string())
}
out
}
///|
/// Builds a style from its `styles` entries (token type, style string), like
/// defining a `Style` subclass in Python. Entries for standard token types
/// that are missing are added (empty), as `StyleMeta` does.
///
/// ```mbt check
/// test {
/// let st = @styles.Style::new([
/// (@token.keyword, "bold #008000"),
/// (@token.keyword_type, "nobold"),
/// ])
/// let kt = st.style_for_token(@token.keyword_type)
/// inspect(kt.color.unwrap_or("-"), content="008000")
/// inspect(kt.bold, content="false")
/// }
/// ```
pub fn Style::new(
styles : Array[(@token.TokenType, String)],
name? : String = "unnamed",
aliases? : Array[String] = [],
background_color? : String? = Some("#ffffff"),
highlight_color? : String? = Some("#ffffcc"),
line_number_color? : String = "inherit",
line_number_background_color? : String = "transparent",
line_number_special_color? : String = "#000000",
line_number_special_background_color? : String = "#ffffc0",
web_style_gallery_exclude? : Bool = false,
) -> Style raise StyleError {
let raw : Map[@token.TokenType, String] = Map::from_array(styles)
for t, _ in @token.standard_names() {
if !raw.contains(t) {
raw[t] = ""
}
}
let defs : Map[@token.TokenType, StyleDef] = Map([])
for ttype, _ in raw {
for tok in ttype.split() {
if defs.contains(tok) {
continue
}
let styledefs = words(raw.get(tok).unwrap_or(""))
let parent = match tok.parent() {
Some(p) => defs.get(p)
None => None
}
let ndef = match parent {
None => empty_def
Some(_) if styledefs.contains("noinherit") && tok != @token.token =>
defs[@token.token]
Some(p) => p
}
defs[tok] = apply_styledefs(ndef, styledefs)
}
}
{
name,
aliases,
background_color,
highlight_color,
line_number_color,
line_number_background_color,
line_number_special_color,
line_number_special_background_color,
web_style_gallery_exclude,
styles: raw,
defs,
}
}
///|
/// The processed definition of `ttype` (Python's `cls._styles[ttype]`).
pub fn Style::style_def(self : Style, ttype : @token.TokenType) -> StyleDef? {
self.defs.get(ttype)
}
///|
/// Python's `style_for_token`: the style of `ttype` with ANSI color names
/// resolved to RGB values.
pub fn Style::style_for_token(
self : Style,
ttype : @token.TokenType,
) -> TokenStyle raise StyleError {
match self.defs.get(ttype) {
Some(d) => token_style(d)
None => raise UnknownToken(ttype)
}
}
///|
fn resolve_ansi(color : String) -> (String, String?) {
let color = deprecated_ansicolors.get(color).unwrap_or(color)
match ansimap.get(color) {
Some(rgb) => (rgb, Some(color))
None => (color, None)
}
}
///|
fn non_empty(s : String) -> String? {
if s == "" {
None
} else {
Some(s)
}
}
///|
fn token_style(t : StyleDef) -> TokenStyle {
let (color, ansicolor) = resolve_ansi(t.color)
let (bgcolor, bgansicolor) = resolve_ansi(t.bgcolor)
{
color: non_empty(color),
bold: t.bold,
italic: t.italic,
underline: t.underline,
bgcolor: non_empty(bgcolor),
border: non_empty(t.border),
roman: t.roman,
sans: t.sans,
mono: t.mono,
ansicolor,
bgansicolor,
}
}
///|
/// Python's `styles_token`: whether the style defines `ttype`.
pub fn Style::styles_token(self : Style, ttype : @token.TokenType) -> Bool {
self.defs.contains(ttype)
}
///|
/// Python's `__iter__`: every token type the style knows, in definition
/// order, with its `style_for_token` dictionary.
pub fn Style::iter(self : Style) -> Iter[(@token.TokenType, TokenStyle)] {
self.defs.iter().map(p => (p.0, token_style(p.1)))
}
///|
/// Python's `list_styles`.
pub fn Style::list_styles(
self : Style,
) -> Array[(@token.TokenType, TokenStyle)] {
self.iter().collect()
}
///|
/// Python's `__len__`: the number of token types the style knows.
pub fn Style::length(self : Style) -> Int {
self.defs.length()
}