diff --git a/src/formatter.mbt b/src/formatter.mbt index bfb4c91..09f871b 100644 --- a/src/formatter.mbt +++ b/src/formatter.mbt @@ -1,30 +1,12 @@ -pub type RecordFormatter = (Record) -> String +pub type RecordFormatter = @utils.RecordFormatter -pub(all) enum ColorMode { - Never - Auto - Always -} +pub type ColorMode = @utils.ColorMode -pub(all) enum ColorSupport { - Basic - TrueColor -} +pub type ColorSupport = @utils.ColorSupport -pub(all) enum StyleMarkupMode { - Disabled - Builtin - Full -} +pub type StyleMarkupMode = @utils.StyleMarkupMode -pub struct TextStyle { - fg : String? - bg : String? - bold : Bool - dim : Bool - italic : Bool - underline : Bool -} +pub type TextStyle = @utils.TextStyle pub fn text_style( fg~ : String? = None, @@ -34,167 +16,39 @@ pub fn text_style( italic~ : Bool = false, underline~ : Bool = false, ) -> TextStyle { - { fg, bg, bold, dim, italic, underline } + @utils.text_style( + fg=fg, + bg=bg, + bold=bold, + dim=dim, + italic=italic, + underline=underline, + ) } -pub struct StyleTagRegistry { - entries : Map[String, TextStyle] -} - -fn normalize_style_tag_name(name : String) -> String { - name.trim().to_lower().to_owned() -} +pub type StyleTagRegistry = @utils.StyleTagRegistry pub fn style_tag_registry() -> StyleTagRegistry { - { entries: {} } -} - -fn merge_text_style(base : TextStyle, overlay : TextStyle) -> TextStyle { - { - fg: match overlay.fg { - Some(_) => overlay.fg - None => base.fg - }, - bg: match overlay.bg { - Some(_) => overlay.bg - None => base.bg - }, - bold: base.bold || overlay.bold, - dim: base.dim || overlay.dim, - italic: base.italic || overlay.italic, - underline: base.underline || overlay.underline, - } -} - -pub fn StyleTagRegistry::set_tag( - self : StyleTagRegistry, - name : String, - style~ : TextStyle = text_style(), - fg~ : String? = None, - bg~ : String? = None, - bold~ : Bool = false, - dim~ : Bool = false, - italic~ : Bool = false, - underline~ : Bool = false, -) -> StyleTagRegistry { - let overlay = text_style(fg=fg, bg=bg, bold=bold, dim=dim, italic=italic, underline=underline) - self.entries[normalize_style_tag_name(name)] = merge_text_style(style, overlay) - self -} - -pub fn StyleTagRegistry::get(self : StyleTagRegistry, name : String) -> TextStyle? { - self.entries.get(normalize_style_tag_name(name)) -} - -pub fn StyleTagRegistry::contains(self : StyleTagRegistry, name : String) -> Bool { - self.entries.contains(normalize_style_tag_name(name)) -} - -pub fn StyleTagRegistry::define_alias( - self : StyleTagRegistry, - name : String, - target : String, -) -> StyleTagRegistry { - match self.get(target) { - Some(style) => self.set_tag(name, style=style) - None => match global_style_tag_registry().get(target) { - Some(style) => self.set_tag(name, style=style) - None => match builtin_text_style_for_tag(normalize_style_tag_name(target)) { - Some(style) => self.set_tag(name, style=style) - None => self - } - } - } + @utils.style_tag_registry() } pub fn default_style_tag_registry() -> StyleTagRegistry { - style_tag_registry() - .set_tag("black", fg=Some("black")) - .set_tag("red", fg=Some("red")) - .set_tag("green", fg=Some("green")) - .set_tag("yellow", fg=Some("yellow")) - .set_tag("blue", fg=Some("blue")) - .set_tag("magenta", fg=Some("magenta")) - .set_tag("cyan", fg=Some("cyan")) - .set_tag("white", fg=Some("white")) - .set_tag("bright_black", fg=Some("bright_black")) - .set_tag("bright_red", fg=Some("bright_red")) - .set_tag("bright_green", fg=Some("bright_green")) - .set_tag("bright_yellow", fg=Some("bright_yellow")) - .set_tag("bright_blue", fg=Some("bright_blue")) - .set_tag("bright_magenta", fg=Some("bright_magenta")) - .set_tag("bright_cyan", fg=Some("bright_cyan")) - .set_tag("bright_white", fg=Some("bright_white")) - .set_tag("accent", fg=Some("bright_cyan"), bold=true) - .set_tag("info", fg=Some("cyan")) - .set_tag("success", fg=Some("green"), bold=true) - .set_tag("warning", fg=Some("yellow"), bold=true) - .set_tag("danger", fg=Some("red"), bold=true) - .set_tag("muted", fg=Some("bright_black"), dim=true) - .set_tag("b", bold=true) - .set_tag("dim", dim=true) - .set_tag("i", italic=true) - .set_tag("u", underline=true) + @utils.default_style_tag_registry() } -let global_style_tag_registry_ref : Ref[StyleTagRegistry] = Ref(style_tag_registry()) - pub fn global_style_tag_registry() -> StyleTagRegistry { - global_style_tag_registry_ref.val + @utils.global_style_tag_registry() } pub fn set_global_style_tag_registry(registry : StyleTagRegistry) -> Unit { - global_style_tag_registry_ref.val = registry + @utils.set_global_style_tag_registry(registry) } pub fn reset_global_style_tag_registry() -> Unit { - global_style_tag_registry_ref.val = style_tag_registry() + @utils.reset_global_style_tag_registry() } -priv struct InlineStyle { - fg_code : String? - bg_code : String? - fg_basic_name : String? - bg_basic_name : String? - bold : Bool - dim : Bool - italic : Bool - underline : Bool -} - -priv struct StyledSegment { - text : String - style : InlineStyle -} - -priv struct StyleFrame { - tag : String - style : InlineStyle -} - -fn code_unit(ch : Char) -> Int { - ch.to_int() -} - -fn code_unit16(ch : UInt16) -> Int { - ch.to_int() -} - -pub struct TextFormatter { - show_timestamp : Bool - show_level : Bool - show_target : Bool - show_fields : Bool - separator : String - field_separator : String - template : String - color_mode : ColorMode - color_support : ColorSupport - style_markup : StyleMarkupMode - target_style_markup : StyleMarkupMode - fields_style_markup : StyleMarkupMode - style_tags : StyleTagRegistry? -} +pub type TextFormatter = @utils.TextFormatter pub fn text_formatter( show_timestamp~ : Bool = true, @@ -211,695 +65,39 @@ pub fn text_formatter( fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, style_tags~ : StyleTagRegistry? = None, ) -> TextFormatter { - { - show_timestamp, - show_level, - show_target, - show_fields, - separator, - field_separator, - template, - color_mode, - color_support, - style_markup, - target_style_markup, - fields_style_markup, - style_tags, - } + @utils.text_formatter( + show_timestamp=show_timestamp, + show_level=show_level, + show_target=show_target, + show_fields=show_fields, + separator=separator, + field_separator=field_separator, + template=template, + color_mode=color_mode, + color_support=color_support, + style_markup=style_markup, + target_style_markup=target_style_markup, + fields_style_markup=fields_style_markup, + style_tags=style_tags, + ) } pub fn color_support_label(support : ColorSupport) -> String { - match support { - ColorSupport::Basic => "basic" - ColorSupport::TrueColor => "truecolor" - } -} - -pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : ColorSupport) -> TextFormatter { - { ..self, color_support } + @utils.color_support_label(support) } pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String { - match mode { - StyleMarkupMode::Disabled => "disabled" - StyleMarkupMode::Builtin => "builtin" - StyleMarkupMode::Full => "full" - } -} - -pub fn TextFormatter::with_style_markup(self : TextFormatter, style_markup : StyleMarkupMode) -> TextFormatter { - { ..self, style_markup } -} - -pub fn TextFormatter::without_style_markup(self : TextFormatter) -> TextFormatter { - { ..self, style_markup: StyleMarkupMode::Disabled } -} - -pub fn TextFormatter::with_target_style_markup( - self : TextFormatter, - style_markup : StyleMarkupMode, -) -> TextFormatter { - { ..self, target_style_markup: style_markup } -} - -pub fn TextFormatter::with_fields_style_markup( - self : TextFormatter, - style_markup : StyleMarkupMode, -) -> TextFormatter { - { ..self, fields_style_markup: style_markup } -} - -pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTagRegistry) -> TextFormatter { - { ..self, style_tags: Some(style_tags) } + @utils.style_markup_mode_label(mode) } pub fn color_mode_label(mode : ColorMode) -> String { - match mode { - ColorMode::Never => "never" - ColorMode::Auto => "auto" - ColorMode::Always => "always" - } -} - -fn use_ansi_color(mode : ColorMode) -> Bool { - match mode { - ColorMode::Never => false - ColorMode::Always => true - ColorMode::Auto => match @env.get_env_var("NO_COLOR") { - Some(_) => false - None => true - } - } -} - -fn ansi_wrap(text : String, code : String, enabled : Bool) -> String { - if !enabled || text == "" { - text - } else { - "\u{001b}[\{code}m\{text}\u{001b}[0m" - } -} - -fn ansi_wrap_with_style(text : String, style : InlineStyle, enabled : Bool) -> String { - if !enabled || text == "" { - text - } else { - let codes : Array[String] = [] - match style.fg_code { - Some(code) => codes.push(code) - None => () - } - match style.bg_code { - Some(code) => codes.push(code) - None => () - } - if style.bold { - codes.push("1") - } - if style.dim { - codes.push("2") - } - if style.italic { - codes.push("3") - } - if style.underline { - codes.push("4") - } - if codes.length() == 0 { - text - } else { - let joined = codes.join(";") - "\u{001b}[\{joined}m\{text}\u{001b}[0m" - } - } -} - -fn default_inline_style() -> InlineStyle { - { - fg_code: None, - bg_code: None, - fg_basic_name: None, - bg_basic_name: None, - bold: false, - dim: false, - italic: false, - underline: false, - } -} - -fn named_color_code(tag : String) -> String? { - match tag { - "black" => Some("30") - "red" => Some("31") - "green" => Some("32") - "yellow" => Some("33") - "blue" => Some("34") - "magenta" => Some("35") - "cyan" => Some("36") - "white" => Some("37") - "bright_black" => Some("90") - "bright_red" => Some("91") - "bright_green" => Some("92") - "bright_yellow" => Some("93") - "bright_blue" => Some("94") - "bright_magenta" => Some("95") - "bright_cyan" => Some("96") - "bright_white" => Some("97") - _ => None - } -} - -fn named_bg_color_code(tag : String) -> String? { - match tag { - "black" => Some("40") - "red" => Some("41") - "green" => Some("42") - "yellow" => Some("43") - "blue" => Some("44") - "magenta" => Some("45") - "cyan" => Some("46") - "white" => Some("47") - "bright_black" => Some("100") - "bright_red" => Some("101") - "bright_green" => Some("102") - "bright_yellow" => Some("103") - "bright_blue" => Some("104") - "bright_magenta" => Some("105") - "bright_cyan" => Some("106") - "bright_white" => Some("107") - _ => None - } -} - -fn basic_name_from_hex(value : String) -> String { - let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2)) - let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4)) - let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6)) - let max_value = if r >= g && r >= b { r } else if g >= b { g } else { b } - if max_value < 48 { - "bright_black" - } else { - let min_value = if r <= g && r <= b { r } else if g <= b { g } else { b } - let spread = max_value - min_value - if spread < 24 { - if max_value > 170 { - "white" - } else if max_value > 96 { - "bright_black" - } else { - "black" - } - } else if r >= g && r >= b { - if g > 96 && b < 96 { - "yellow" - } else if b > 96 && g < 96 { - "magenta" - } else { - "red" - } - } else if g >= r && g >= b { - if r > 96 && b < 96 { - "yellow" - } else if b > 96 && r < 96 { - "cyan" - } else { - "green" - } - } else { - if r > 96 && g < 96 { - "magenta" - } else if g > 96 && r < 96 { - "cyan" - } else { - "blue" - } - } - } -} - -fn resolve_inline_color_code( - formatter : TextFormatter, - basic_name : String?, - truecolor_code : String, -) -> String { - match formatter.color_support { - ColorSupport::TrueColor => truecolor_code - ColorSupport::Basic => match basic_name { - Some(name) => named_code_from_basic_name(name).unwrap_or(truecolor_code) - None => truecolor_code - } - } -} - -fn named_code_from_basic_name(name : String) -> String? { - named_color_code(name) -} - -fn named_bg_code_from_basic_name(name : String) -> String? { - named_bg_color_code(name) -} - -fn resolve_inline_bg_code( - formatter : TextFormatter, - basic_name : String?, - truecolor_code : String, -) -> String { - match formatter.color_support { - ColorSupport::TrueColor => truecolor_code - ColorSupport::Basic => match basic_name { - Some(name) => named_bg_code_from_basic_name(name).unwrap_or(truecolor_code) - None => truecolor_code - } - } -} - -fn is_hex_color(value : String) -> Bool { - if value.length() != 7 { - return false - } - if value.unsafe_get(0) != '#' { - return false - } - for i = 1; i < value.length(); i = i + 1 { - let ch = value.unsafe_get(i) - let is_digit = ch >= '0' && ch <= '9' - let is_lower = ch >= 'a' && ch <= 'f' - let is_upper = ch >= 'A' && ch <= 'F' - if !(is_digit || is_lower || is_upper) { - return false - } - } - true -} - -fn hex_to_int(ch : UInt16) -> Int { - let code = code_unit16(ch) - if code >= code_unit('0') && code <= code_unit('9') { - code_unit16(ch) - code_unit('0') - } else if code >= code_unit('a') && code <= code_unit('f') { - code_unit16(ch) - code_unit('a') + 10 - } else { - code_unit16(ch) - code_unit('A') + 10 - } -} - -fn hex_pair_to_int(high : UInt16, low : UInt16) -> Int { - hex_to_int(high) * 16 + hex_to_int(low) -} - -fn rgb_fg_code(value : String) -> String { - let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2)) - let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4)) - let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6)) - "38;2;\{r};\{g};\{b}" -} - -fn rgb_bg_code(value : String) -> String { - let r = hex_pair_to_int(value.unsafe_get(4), value.unsafe_get(5)) - let g = hex_pair_to_int(value.unsafe_get(6), value.unsafe_get(7)) - let b = hex_pair_to_int(value.unsafe_get(8), value.unsafe_get(9)) - "48;2;\{r};\{g};\{b}" -} - -fn rgb_bg_code_from_hex(value : String) -> String { - let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2)) - let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4)) - let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6)) - "48;2;\{r};\{g};\{b}" -} - -fn inline_style_from_text_style( - base : InlineStyle, - style : TextStyle, - formatter : TextFormatter, -) -> InlineStyle? { - let fg = match style.fg { - None => Some((base.fg_code, base.fg_basic_name)) - Some(value) => { - let normalized = normalize_style_tag_name(value) - match named_color_code(normalized) { - Some(code) => Some((Some(code), Some(normalized))) - None => if is_hex_color(value) { - let basic_name = basic_name_from_hex(value) - Some(( - Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(value))), - Some(basic_name), - )) - } else { - None - } - } - } - } - let bg = match style.bg { - None => Some((base.bg_code, base.bg_basic_name)) - Some(value) => { - let normalized = normalize_style_tag_name(value) - match named_bg_color_code(normalized) { - Some(code) => { - let bg_name = if normalized.has_prefix("bright_") { - normalized - } else { - normalized - } - Some((Some(code), Some(bg_name))) - } - None => if is_hex_color(value) { - let basic_name = basic_name_from_hex(value) - Some(( - Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code_from_hex(value))), - Some(basic_name), - )) - } else { - None - } - } - } - } - match fg { - Some((next_fg_code, next_fg_name)) => match bg { - Some((next_bg_code, next_bg_name)) => Some({ - fg_code: next_fg_code, - bg_code: next_bg_code, - fg_basic_name: next_fg_name, - bg_basic_name: next_bg_name, - bold: base.bold || style.bold, - dim: base.dim || style.dim, - italic: base.italic || style.italic, - underline: base.underline || style.underline, - }) - None => None - } - None => None - } -} - -fn builtin_text_style_for_tag(tag : String) -> TextStyle? { - match normalize_style_tag_name(tag) { - "black" => Some(text_style(fg=Some("black"))) - "red" => Some(text_style(fg=Some("red"))) - "green" => Some(text_style(fg=Some("green"))) - "yellow" => Some(text_style(fg=Some("yellow"))) - "blue" => Some(text_style(fg=Some("blue"))) - "magenta" => Some(text_style(fg=Some("magenta"))) - "cyan" => Some(text_style(fg=Some("cyan"))) - "white" => Some(text_style(fg=Some("white"))) - "bright_black" => Some(text_style(fg=Some("bright_black"))) - "bright_red" => Some(text_style(fg=Some("bright_red"))) - "bright_green" => Some(text_style(fg=Some("bright_green"))) - "bright_yellow" => Some(text_style(fg=Some("bright_yellow"))) - "bright_blue" => Some(text_style(fg=Some("bright_blue"))) - "bright_magenta" => Some(text_style(fg=Some("bright_magenta"))) - "bright_cyan" => Some(text_style(fg=Some("bright_cyan"))) - "bright_white" => Some(text_style(fg=Some("bright_white"))) - "accent" => Some(text_style(fg=Some("bright_cyan"), bold=true)) - "info" => Some(text_style(fg=Some("cyan"))) - "success" => Some(text_style(fg=Some("green"), bold=true)) - "warning" => Some(text_style(fg=Some("yellow"), bold=true)) - "danger" => Some(text_style(fg=Some("red"), bold=true)) - "muted" => Some(text_style(fg=Some("bright_black"), dim=true)) - "b" => Some(text_style(bold=true)) - "dim" => Some(text_style(dim=true)) - "i" => Some(text_style(italic=true)) - "u" => Some(text_style(underline=true)) - _ => None - } -} - -fn resolve_registered_text_style(tag : String, formatter : TextFormatter) -> TextStyle? { - let normalized = normalize_style_tag_name(tag) - match formatter.style_markup { - StyleMarkupMode::Builtin => return builtin_text_style_for_tag(normalized) - _ => () - } - match formatter.style_tags { - Some(registry) => match registry.get(normalized) { - Some(style) => Some(style) - None => match global_style_tag_registry().get(normalized) { - Some(style) => Some(style) - None => builtin_text_style_for_tag(normalized) - } - } - None => match global_style_tag_registry().get(normalized) { - Some(style) => Some(style) - None => builtin_text_style_for_tag(normalized) - } - } -} - -fn apply_inline_tag(style : InlineStyle, tag : String, formatter : TextFormatter) -> InlineStyle? { - let normalized = normalize_style_tag_name(tag) - if is_hex_color(tag) { - let basic_name = basic_name_from_hex(tag) - Some({ - ..style, - fg_code: Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(tag))), - fg_basic_name: Some(basic_name), - }) - } else if normalized.length() == 10 && normalized.has_prefix("bg:") && is_hex_color(normalized[3:].to_owned()) { - let basic_name = basic_name_from_hex(normalized[3:].to_owned()) - Some({ - ..style, - bg_code: Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code(normalized))), - bg_basic_name: Some(basic_name), - }) - } else { - match resolve_registered_text_style(normalized, formatter) { - Some(spec) => inline_style_from_text_style(style, spec, formatter) - None => None - } - } -} - -fn push_plain_segment( - segments : Array[StyledSegment], - buffer : StringBuilder, - style : InlineStyle, -) -> Unit { - let text = buffer.to_string() - if text != "" { - segments.push({ text, style }) - buffer.reset() - } -} - -fn pop_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool { - let normalized = normalize_style_tag_name(tag) - if stack.length() <= 1 { - return false - } - for i = stack.length() - 1; i > 0; i = i - 1 { - if stack[i].tag == normalized { - while stack.length() - 1 >= i { - ignore(stack.pop()) - } - return true - } - } - false -} - -fn has_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool { - let normalized = normalize_style_tag_name(tag) - if stack.length() <= 1 { - return false - } - for i = stack.length() - 1; i > 0; i = i - 1 { - if stack[i].tag == normalized { - return true - } - } - false -} - -fn parse_inline_markup(input : String, formatter : TextFormatter) -> Array[StyledSegment] { - let segments : Array[StyledSegment] = [] - let buffer = StringBuilder::new() - let stack : Array[StyleFrame] = [{ tag: "", style: default_inline_style() }] - let chars = input.to_array() - let current_style = fn() { stack[stack.length() - 1].style } - let flush = fn() { push_plain_segment(segments, buffer, current_style()) } - let append_raw = fn(start : Int, finish : Int) { - for i = start; i < finish; i = i + 1 { - buffer.write_char(chars.unsafe_get(i)) - } - } - let find_tag_end = fn(start : Int) -> Int { - for i = start; i < chars.length(); i = i + 1 { - if chars.unsafe_get(i) == '>' { - return i - } - } - -1 - } - for i = 0; i < chars.length(); { - if chars.unsafe_get(i) != '<' { - buffer.write_char(chars.unsafe_get(i)) - continue i + 1 - } - let end = find_tag_end(i + 1) - if end == -1 { - buffer.write_char(chars[i]) - continue i + 1 - } - let tag = input[i + 1:end].to_owned() - if tag == "/" { - if stack.length() > 1 { - flush() - ignore(stack.pop()) - } else { - append_raw(i, end + 1) - } - continue end + 1 - } - if tag.has_prefix("/") { - let close_tag = tag[1:].to_owned() - if close_tag != "" && has_named_style_frame(stack, close_tag) { - flush() - ignore(pop_named_style_frame(stack, close_tag)) - } else { - append_raw(i, end + 1) - } - continue end + 1 - } - match apply_inline_tag(current_style(), tag, formatter) { - Some(next_style) => { - flush() - stack.push({ tag: normalize_style_tag_name(tag), style: next_style }) - } - None => append_raw(i, end + 1) - } - continue end + 1 - } - flush() - segments -} - -fn render_styled_text(text : String, formatter : TextFormatter, mode : StyleMarkupMode) -> String { - match mode { - StyleMarkupMode::Disabled => return text - _ => () - } - let enabled = use_ansi_color(formatter.color_mode) - let scoped = { ..formatter, style_markup: mode } - let segments = parse_inline_markup(text, scoped) - let out = StringBuilder::new() - for segment in segments { - out.write_string(ansi_wrap_with_style(segment.text, segment.style, enabled)) - } - out.to_string() -} - -fn render_inline_markup(message : String, formatter : TextFormatter) -> String { - render_styled_text(message, formatter, formatter.style_markup) -} - -fn level_ansi_code(level : Level) -> String { - match level { - Level::Trace => "90" - Level::Debug => "36" - Level::Info => "32" - Level::Warn => "33" - Level::Error => "31;1" - } -} - -fn fields_to_json(fields : Array[Field]) -> Json { - let obj : Map[String, Json] = {} - for item in fields { - obj[item.key] = Json::string(item.value) - } - Json::object(obj) -} - -fn timestamp_text(rec : Record, formatter : TextFormatter) -> String { - if formatter.show_timestamp && rec.timestamp_ms != 0UL { - ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode)) - } else { - "" - } -} - -fn level_text(rec : Record, formatter : TextFormatter) -> String { - if formatter.show_level { - ansi_wrap(rec.level.label(), level_ansi_code(rec.level), use_ansi_color(formatter.color_mode)) - } else { - "" - } -} - -fn target_text(rec : Record, formatter : TextFormatter) -> String { - if formatter.show_target && rec.target != "" { - let rendered = render_styled_text(rec.target, formatter, formatter.target_style_markup) - ansi_wrap(rendered, "34", use_ansi_color(formatter.color_mode)) - } else { - "" - } -} - -fn format_field_text(field : Field, formatter : TextFormatter) -> String { - let value = render_styled_text(field.value, formatter, formatter.fields_style_markup) - "\{field.key}=\{value}" -} - -fn fields_text(rec : Record, formatter : TextFormatter) -> String { - if formatter.show_fields && rec.fields.length() != 0 { - let content = rec.fields.map(fn(field) { format_field_text(field, formatter) }).join(formatter.field_separator) - ansi_wrap(content, "35", use_ansi_color(formatter.color_mode)) - } else { - "" - } -} - -fn render_template(rec : Record, formatter : TextFormatter) -> String { - formatter.template - .replace_all(old="{timestamp}", new=timestamp_text(rec, formatter)) - .replace_all(old="{timestamp_ms}", new=timestamp_text(rec, formatter)) - .replace_all(old="{level}", new=level_text(rec, formatter)) - .replace_all(old="{target}", new=target_text(rec, formatter)) - .replace_all(old="{message}", new=render_inline_markup(rec.message, formatter)) - .replace_all(old="{fields}", new=fields_text(rec, formatter)) - .trim() - .to_owned() + @utils.color_mode_label(mode) } pub fn format_text(rec : Record, formatter~ : TextFormatter = text_formatter()) -> String { - if formatter.template != "" { - return render_template(rec, formatter) - } - let parts : Array[String] = [] - if formatter.show_timestamp && rec.timestamp_ms != 0UL { - parts.push("[\{timestamp_text(rec, formatter)}]") - } - if formatter.show_level { - parts.push("[\{level_text(rec, formatter)}]") - } - if formatter.show_target && rec.target != "" { - parts.push("[\{target_text(rec, formatter)}]") - } - parts.push(render_inline_markup(rec.message, formatter)) - let base = parts.join(formatter.separator) - if !formatter.show_fields || rec.fields.length() == 0 { - base - } else { - let details = fields_text(rec, formatter) - "\{base}\{formatter.separator}\{details}" - } + @utils.format_text(rec, formatter=formatter) } pub fn format_json(rec : Record) -> String { - let obj : Map[String, Json] = { - "level": Json::string(rec.level.label()), - "message": Json::string(rec.message), - "fields": fields_to_json(rec.fields), - } - if rec.timestamp_ms != 0UL { - obj["timestamp_ms"] = rec.timestamp_ms.to_json() - } - if rec.target == "" { - Json::object(obj).stringify() - } else { - obj["target"] = Json::string(rec.target) - Json::object(obj).stringify() - } + @utils.format_json(rec) } diff --git a/src/utils/formatter.mbt b/src/utils/formatter.mbt new file mode 100644 index 0000000..61c81f1 --- /dev/null +++ b/src/utils/formatter.mbt @@ -0,0 +1,975 @@ +pub type RecordFormatter = (@core.Record) -> String + +pub(all) enum ColorMode { + Never + Auto + Always +} + +pub(all) enum ColorSupport { + Basic + TrueColor +} + +pub(all) enum StyleMarkupMode { + Disabled + Builtin + Full +} + +pub struct TextStyle { + fg : String? + bg : String? + bold : Bool + dim : Bool + italic : Bool + underline : Bool +} + +pub fn text_style( + fg~ : String? = None, + bg~ : String? = None, + bold~ : Bool = false, + dim~ : Bool = false, + italic~ : Bool = false, + underline~ : Bool = false, +) -> TextStyle { + { fg, bg, bold, dim, italic, underline } +} + +pub struct StyleTagRegistry { + entries : Map[String, TextStyle] +} + +fn normalize_style_tag_name(name : String) -> String { + name.trim().to_lower().to_owned() +} + +pub fn style_tag_registry() -> StyleTagRegistry { + { entries: {} } +} + +fn merge_text_style(base : TextStyle, overlay : TextStyle) -> TextStyle { + { + fg: match overlay.fg { + Some(_) => overlay.fg + None => base.fg + }, + bg: match overlay.bg { + Some(_) => overlay.bg + None => base.bg + }, + bold: base.bold || overlay.bold, + dim: base.dim || overlay.dim, + italic: base.italic || overlay.italic, + underline: base.underline || overlay.underline, + } +} + +pub fn StyleTagRegistry::set_tag( + self : StyleTagRegistry, + name : String, + style~ : TextStyle = text_style(), + fg~ : String? = None, + bg~ : String? = None, + bold~ : Bool = false, + dim~ : Bool = false, + italic~ : Bool = false, + underline~ : Bool = false, +) -> StyleTagRegistry { + let overlay = text_style(fg=fg, bg=bg, bold=bold, dim=dim, italic=italic, underline=underline) + self.entries[normalize_style_tag_name(name)] = merge_text_style(style, overlay) + self +} + +pub fn StyleTagRegistry::get(self : StyleTagRegistry, name : String) -> TextStyle? { + self.entries.get(normalize_style_tag_name(name)) +} + +pub fn StyleTagRegistry::contains(self : StyleTagRegistry, name : String) -> Bool { + self.entries.contains(normalize_style_tag_name(name)) +} + +pub fn StyleTagRegistry::define_alias( + self : StyleTagRegistry, + name : String, + target : String, +) -> StyleTagRegistry { + match self.get(target) { + Some(style) => self.set_tag(name, style=style) + None => match global_style_tag_registry().get(target) { + Some(style) => self.set_tag(name, style=style) + None => match builtin_text_style_for_tag(normalize_style_tag_name(target)) { + Some(style) => self.set_tag(name, style=style) + None => self + } + } + } +} + +pub fn default_style_tag_registry() -> StyleTagRegistry { + style_tag_registry() + .set_tag("black", fg=Some("black")) + .set_tag("red", fg=Some("red")) + .set_tag("green", fg=Some("green")) + .set_tag("yellow", fg=Some("yellow")) + .set_tag("blue", fg=Some("blue")) + .set_tag("magenta", fg=Some("magenta")) + .set_tag("cyan", fg=Some("cyan")) + .set_tag("white", fg=Some("white")) + .set_tag("bright_black", fg=Some("bright_black")) + .set_tag("bright_red", fg=Some("bright_red")) + .set_tag("bright_green", fg=Some("bright_green")) + .set_tag("bright_yellow", fg=Some("bright_yellow")) + .set_tag("bright_blue", fg=Some("bright_blue")) + .set_tag("bright_magenta", fg=Some("bright_magenta")) + .set_tag("bright_cyan", fg=Some("bright_cyan")) + .set_tag("bright_white", fg=Some("bright_white")) + .set_tag("accent", fg=Some("bright_cyan"), bold=true) + .set_tag("info", fg=Some("cyan")) + .set_tag("success", fg=Some("green"), bold=true) + .set_tag("warning", fg=Some("yellow"), bold=true) + .set_tag("danger", fg=Some("red"), bold=true) + .set_tag("muted", fg=Some("bright_black"), dim=true) + .set_tag("b", bold=true) + .set_tag("dim", dim=true) + .set_tag("i", italic=true) + .set_tag("u", underline=true) +} + +let global_style_tag_registry_ref : Ref[StyleTagRegistry] = Ref(style_tag_registry()) + +pub fn global_style_tag_registry() -> StyleTagRegistry { + global_style_tag_registry_ref.val +} + +pub fn set_global_style_tag_registry(registry : StyleTagRegistry) -> Unit { + global_style_tag_registry_ref.val = registry +} + +pub fn reset_global_style_tag_registry() -> Unit { + global_style_tag_registry_ref.val = style_tag_registry() +} + +priv struct InlineStyle { + fg_code : String? + bg_code : String? + fg_basic_name : String? + bg_basic_name : String? + bold : Bool + dim : Bool + italic : Bool + underline : Bool +} + +priv struct StyledSegment { + text : String + style : InlineStyle +} + +priv struct StyleFrame { + tag : String + style : InlineStyle +} + +fn code_unit(ch : Char) -> Int { + ch.to_int() +} + +fn code_unit16(ch : UInt16) -> Int { + ch.to_int() +} + +pub struct TextFormatter { + show_timestamp : Bool + show_level : Bool + show_target : Bool + show_fields : Bool + separator : String + field_separator : String + template : String + color_mode : ColorMode + color_support : ColorSupport + style_markup : StyleMarkupMode + target_style_markup : StyleMarkupMode + fields_style_markup : StyleMarkupMode + style_tags : StyleTagRegistry? +} + +pub fn text_formatter( + show_timestamp~ : Bool = true, + show_level~ : Bool = true, + show_target~ : Bool = true, + show_fields~ : Bool = true, + separator~ : String = " ", + field_separator~ : String = " ", + template~ : String = "", + color_mode~ : ColorMode = ColorMode::Never, + color_support~ : ColorSupport = ColorSupport::TrueColor, + style_markup~ : StyleMarkupMode = StyleMarkupMode::Full, + target_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, + fields_style_markup~ : StyleMarkupMode = StyleMarkupMode::Disabled, + style_tags~ : StyleTagRegistry? = None, +) -> TextFormatter { + { + show_timestamp, + show_level, + show_target, + show_fields, + separator, + field_separator, + template, + color_mode, + color_support, + style_markup, + target_style_markup, + fields_style_markup, + style_tags, + } +} + +pub fn color_support_label(support : ColorSupport) -> String { + match support { + ColorSupport::Basic => "basic" + ColorSupport::TrueColor => "truecolor" + } +} + +pub fn TextFormatter::with_color_support(self : TextFormatter, color_support : ColorSupport) -> TextFormatter { + text_formatter( + show_timestamp=self.show_timestamp, + show_level=self.show_level, + show_target=self.show_target, + show_fields=self.show_fields, + separator=self.separator, + field_separator=self.field_separator, + template=self.template, + color_mode=self.color_mode, + color_support=color_support, + style_markup=self.style_markup, + target_style_markup=self.target_style_markup, + fields_style_markup=self.fields_style_markup, + style_tags=self.style_tags, + ) +} + +pub fn style_markup_mode_label(mode : StyleMarkupMode) -> String { + match mode { + StyleMarkupMode::Disabled => "disabled" + StyleMarkupMode::Builtin => "builtin" + StyleMarkupMode::Full => "full" + } +} + +pub fn TextFormatter::with_style_markup(self : TextFormatter, style_markup : StyleMarkupMode) -> TextFormatter { + text_formatter( + show_timestamp=self.show_timestamp, + show_level=self.show_level, + show_target=self.show_target, + show_fields=self.show_fields, + separator=self.separator, + field_separator=self.field_separator, + template=self.template, + color_mode=self.color_mode, + color_support=self.color_support, + style_markup=style_markup, + target_style_markup=self.target_style_markup, + fields_style_markup=self.fields_style_markup, + style_tags=self.style_tags, + ) +} + +pub fn TextFormatter::without_style_markup(self : TextFormatter) -> TextFormatter { + self.with_style_markup(StyleMarkupMode::Disabled) +} + +pub fn TextFormatter::with_target_style_markup( + self : TextFormatter, + style_markup : StyleMarkupMode, +) -> TextFormatter { + text_formatter( + show_timestamp=self.show_timestamp, + show_level=self.show_level, + show_target=self.show_target, + show_fields=self.show_fields, + separator=self.separator, + field_separator=self.field_separator, + template=self.template, + color_mode=self.color_mode, + color_support=self.color_support, + style_markup=self.style_markup, + target_style_markup=style_markup, + fields_style_markup=self.fields_style_markup, + style_tags=self.style_tags, + ) +} + +pub fn TextFormatter::with_fields_style_markup( + self : TextFormatter, + style_markup : StyleMarkupMode, +) -> TextFormatter { + text_formatter( + show_timestamp=self.show_timestamp, + show_level=self.show_level, + show_target=self.show_target, + show_fields=self.show_fields, + separator=self.separator, + field_separator=self.field_separator, + template=self.template, + color_mode=self.color_mode, + color_support=self.color_support, + style_markup=self.style_markup, + target_style_markup=self.target_style_markup, + fields_style_markup=style_markup, + style_tags=self.style_tags, + ) +} + +pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTagRegistry) -> TextFormatter { + text_formatter( + show_timestamp=self.show_timestamp, + show_level=self.show_level, + show_target=self.show_target, + show_fields=self.show_fields, + separator=self.separator, + field_separator=self.field_separator, + template=self.template, + color_mode=self.color_mode, + color_support=self.color_support, + style_markup=self.style_markup, + target_style_markup=self.target_style_markup, + fields_style_markup=self.fields_style_markup, + style_tags=Some(style_tags), + ) +} + +pub fn color_mode_label(mode : ColorMode) -> String { + match mode { + ColorMode::Never => "never" + ColorMode::Auto => "auto" + ColorMode::Always => "always" + } +} + +fn use_ansi_color(mode : ColorMode) -> Bool { + match mode { + ColorMode::Never => false + ColorMode::Always => true + ColorMode::Auto => match @env.get_env_var("NO_COLOR") { + Some(_) => false + None => true + } + } +} + +fn ansi_wrap(text : String, code : String, enabled : Bool) -> String { + if !enabled || text == "" { + text + } else { + "\u{001b}[\{code}m\{text}\u{001b}[0m" + } +} + +fn ansi_wrap_with_style(text : String, style : InlineStyle, enabled : Bool) -> String { + if !enabled || text == "" { + text + } else { + let codes : Array[String] = [] + match style.fg_code { + Some(code) => codes.push(code) + None => () + } + match style.bg_code { + Some(code) => codes.push(code) + None => () + } + if style.bold { + codes.push("1") + } + if style.dim { + codes.push("2") + } + if style.italic { + codes.push("3") + } + if style.underline { + codes.push("4") + } + if codes.length() == 0 { + text + } else { + let joined = codes.join(";") + "\u{001b}[\{joined}m\{text}\u{001b}[0m" + } + } +} + +fn default_inline_style() -> InlineStyle { + { + fg_code: None, + bg_code: None, + fg_basic_name: None, + bg_basic_name: None, + bold: false, + dim: false, + italic: false, + underline: false, + } +} + +fn named_color_code(tag : String) -> String? { + match tag { + "black" => Some("30") + "red" => Some("31") + "green" => Some("32") + "yellow" => Some("33") + "blue" => Some("34") + "magenta" => Some("35") + "cyan" => Some("36") + "white" => Some("37") + "bright_black" => Some("90") + "bright_red" => Some("91") + "bright_green" => Some("92") + "bright_yellow" => Some("93") + "bright_blue" => Some("94") + "bright_magenta" => Some("95") + "bright_cyan" => Some("96") + "bright_white" => Some("97") + _ => None + } +} + +fn named_bg_color_code(tag : String) -> String? { + match tag { + "black" => Some("40") + "red" => Some("41") + "green" => Some("42") + "yellow" => Some("43") + "blue" => Some("44") + "magenta" => Some("45") + "cyan" => Some("46") + "white" => Some("47") + "bright_black" => Some("100") + "bright_red" => Some("101") + "bright_green" => Some("102") + "bright_yellow" => Some("103") + "bright_blue" => Some("104") + "bright_magenta" => Some("105") + "bright_cyan" => Some("106") + "bright_white" => Some("107") + _ => None + } +} + +fn basic_name_from_hex(value : String) -> String { + let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2)) + let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4)) + let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6)) + let max_value = if r >= g && r >= b { r } else if g >= b { g } else { b } + if max_value < 48 { + "bright_black" + } else { + let min_value = if r <= g && r <= b { r } else if g <= b { g } else { b } + let spread = max_value - min_value + if spread < 24 { + if max_value > 170 { + "white" + } else if max_value > 96 { + "bright_black" + } else { + "black" + } + } else if r >= g && r >= b { + if g > 96 && b < 96 { + "yellow" + } else if b > 96 && g < 96 { + "magenta" + } else { + "red" + } + } else if g >= r && g >= b { + if r > 96 && b < 96 { + "yellow" + } else if b > 96 && r < 96 { + "cyan" + } else { + "green" + } + } else { + if r > 96 && g < 96 { + "magenta" + } else if g > 96 && r < 96 { + "cyan" + } else { + "blue" + } + } + } +} + +fn resolve_inline_color_code( + formatter : TextFormatter, + basic_name : String?, + truecolor_code : String, +) -> String { + match formatter.color_support { + ColorSupport::TrueColor => truecolor_code + ColorSupport::Basic => match basic_name { + Some(name) => named_code_from_basic_name(name).unwrap_or(truecolor_code) + None => truecolor_code + } + } +} + +fn named_code_from_basic_name(name : String) -> String? { + named_color_code(name) +} + +fn named_bg_code_from_basic_name(name : String) -> String? { + named_bg_color_code(name) +} + +fn resolve_inline_bg_code( + formatter : TextFormatter, + basic_name : String?, + truecolor_code : String, +) -> String { + match formatter.color_support { + ColorSupport::TrueColor => truecolor_code + ColorSupport::Basic => match basic_name { + Some(name) => named_bg_code_from_basic_name(name).unwrap_or(truecolor_code) + None => truecolor_code + } + } +} + +fn is_hex_color(value : String) -> Bool { + if value.length() != 7 { + return false + } + if value.unsafe_get(0) != '#' { + return false + } + for i = 1; i < value.length(); i = i + 1 { + let ch = value.unsafe_get(i) + let is_digit = ch >= '0' && ch <= '9' + let is_lower = ch >= 'a' && ch <= 'f' + let is_upper = ch >= 'A' && ch <= 'F' + if !(is_digit || is_lower || is_upper) { + return false + } + } + true +} + +fn hex_to_int(ch : UInt16) -> Int { + let code = code_unit16(ch) + if code >= code_unit('0') && code <= code_unit('9') { + code_unit16(ch) - code_unit('0') + } else if code >= code_unit('a') && code <= code_unit('f') { + code_unit16(ch) - code_unit('a') + 10 + } else { + code_unit16(ch) - code_unit('A') + 10 + } +} + +fn hex_pair_to_int(high : UInt16, low : UInt16) -> Int { + hex_to_int(high) * 16 + hex_to_int(low) +} + +fn rgb_fg_code(value : String) -> String { + let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2)) + let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4)) + let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6)) + "38;2;\{r};\{g};\{b}" +} + +fn rgb_bg_code(value : String) -> String { + let r = hex_pair_to_int(value.unsafe_get(4), value.unsafe_get(5)) + let g = hex_pair_to_int(value.unsafe_get(6), value.unsafe_get(7)) + let b = hex_pair_to_int(value.unsafe_get(8), value.unsafe_get(9)) + "48;2;\{r};\{g};\{b}" +} + +fn rgb_bg_code_from_hex(value : String) -> String { + let r = hex_pair_to_int(value.unsafe_get(1), value.unsafe_get(2)) + let g = hex_pair_to_int(value.unsafe_get(3), value.unsafe_get(4)) + let b = hex_pair_to_int(value.unsafe_get(5), value.unsafe_get(6)) + "48;2;\{r};\{g};\{b}" +} + +fn inline_style_from_text_style( + base : InlineStyle, + style : TextStyle, + formatter : TextFormatter, +) -> InlineStyle? { + let fg = match style.fg { + None => Some((base.fg_code, base.fg_basic_name)) + Some(value) => { + let normalized = normalize_style_tag_name(value) + match named_color_code(normalized) { + Some(code) => Some((Some(code), Some(normalized))) + None => if is_hex_color(value) { + let basic_name = basic_name_from_hex(value) + Some(( + Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(value))), + Some(basic_name), + )) + } else { + None + } + } + } + } + let bg = match style.bg { + None => Some((base.bg_code, base.bg_basic_name)) + Some(value) => { + let normalized = normalize_style_tag_name(value) + match named_bg_color_code(normalized) { + Some(code) => { + let bg_name = if normalized.has_prefix("bright_") { + normalized + } else { + normalized + } + Some((Some(code), Some(bg_name))) + } + None => if is_hex_color(value) { + let basic_name = basic_name_from_hex(value) + Some(( + Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code_from_hex(value))), + Some(basic_name), + )) + } else { + None + } + } + } + } + match fg { + Some((next_fg_code, next_fg_name)) => match bg { + Some((next_bg_code, next_bg_name)) => Some({ + fg_code: next_fg_code, + bg_code: next_bg_code, + fg_basic_name: next_fg_name, + bg_basic_name: next_bg_name, + bold: base.bold || style.bold, + dim: base.dim || style.dim, + italic: base.italic || style.italic, + underline: base.underline || style.underline, + }) + None => None + } + None => None + } +} + +fn builtin_text_style_for_tag(tag : String) -> TextStyle? { + match normalize_style_tag_name(tag) { + "black" => Some(text_style(fg=Some("black"))) + "red" => Some(text_style(fg=Some("red"))) + "green" => Some(text_style(fg=Some("green"))) + "yellow" => Some(text_style(fg=Some("yellow"))) + "blue" => Some(text_style(fg=Some("blue"))) + "magenta" => Some(text_style(fg=Some("magenta"))) + "cyan" => Some(text_style(fg=Some("cyan"))) + "white" => Some(text_style(fg=Some("white"))) + "bright_black" => Some(text_style(fg=Some("bright_black"))) + "bright_red" => Some(text_style(fg=Some("bright_red"))) + "bright_green" => Some(text_style(fg=Some("bright_green"))) + "bright_yellow" => Some(text_style(fg=Some("bright_yellow"))) + "bright_blue" => Some(text_style(fg=Some("bright_blue"))) + "bright_magenta" => Some(text_style(fg=Some("bright_magenta"))) + "bright_cyan" => Some(text_style(fg=Some("bright_cyan"))) + "bright_white" => Some(text_style(fg=Some("bright_white"))) + "accent" => Some(text_style(fg=Some("bright_cyan"), bold=true)) + "info" => Some(text_style(fg=Some("cyan"))) + "success" => Some(text_style(fg=Some("green"), bold=true)) + "warning" => Some(text_style(fg=Some("yellow"), bold=true)) + "danger" => Some(text_style(fg=Some("red"), bold=true)) + "muted" => Some(text_style(fg=Some("bright_black"), dim=true)) + "b" => Some(text_style(bold=true)) + "dim" => Some(text_style(dim=true)) + "i" => Some(text_style(italic=true)) + "u" => Some(text_style(underline=true)) + _ => None + } +} + +fn resolve_registered_text_style(tag : String, formatter : TextFormatter) -> TextStyle? { + let normalized = normalize_style_tag_name(tag) + match formatter.style_markup { + StyleMarkupMode::Builtin => return builtin_text_style_for_tag(normalized) + _ => () + } + match formatter.style_tags { + Some(registry) => match registry.get(normalized) { + Some(style) => Some(style) + None => match global_style_tag_registry().get(normalized) { + Some(style) => Some(style) + None => builtin_text_style_for_tag(normalized) + } + } + None => match global_style_tag_registry().get(normalized) { + Some(style) => Some(style) + None => builtin_text_style_for_tag(normalized) + } + } +} + +fn apply_inline_tag(style : InlineStyle, tag : String, formatter : TextFormatter) -> InlineStyle? { + let normalized = normalize_style_tag_name(tag) + if is_hex_color(tag) { + let basic_name = basic_name_from_hex(tag) + Some({ + ..style, + fg_code: Some(resolve_inline_color_code(formatter, Some(basic_name), rgb_fg_code(tag))), + fg_basic_name: Some(basic_name), + }) + } else if normalized.length() == 10 && normalized.has_prefix("bg:") && is_hex_color(normalized[3:].to_owned()) { + let basic_name = basic_name_from_hex(normalized[3:].to_owned()) + Some({ + ..style, + bg_code: Some(resolve_inline_bg_code(formatter, Some(basic_name), rgb_bg_code(normalized))), + bg_basic_name: Some(basic_name), + }) + } else { + match resolve_registered_text_style(normalized, formatter) { + Some(spec) => inline_style_from_text_style(style, spec, formatter) + None => None + } + } +} + +fn push_plain_segment( + segments : Array[StyledSegment], + buffer : StringBuilder, + style : InlineStyle, +) -> Unit { + let text = buffer.to_string() + if text != "" { + segments.push({ text, style }) + buffer.reset() + } +} + +fn pop_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool { + let normalized = normalize_style_tag_name(tag) + if stack.length() <= 1 { + return false + } + for i = stack.length() - 1; i > 0; i = i - 1 { + if stack[i].tag == normalized { + while stack.length() - 1 >= i { + ignore(stack.pop()) + } + return true + } + } + false +} + +fn has_named_style_frame(stack : Array[StyleFrame], tag : String) -> Bool { + let normalized = normalize_style_tag_name(tag) + if stack.length() <= 1 { + return false + } + for i = stack.length() - 1; i > 0; i = i - 1 { + if stack[i].tag == normalized { + return true + } + } + false +} + +fn parse_inline_markup(input : String, formatter : TextFormatter) -> Array[StyledSegment] { + let segments : Array[StyledSegment] = [] + let buffer = StringBuilder::new() + let stack : Array[StyleFrame] = [{ tag: "", style: default_inline_style() }] + let chars = input.to_array() + let current_style = fn() { stack[stack.length() - 1].style } + let flush = fn() { push_plain_segment(segments, buffer, current_style()) } + let append_raw = fn(start : Int, finish : Int) { + for i = start; i < finish; i = i + 1 { + buffer.write_char(chars.unsafe_get(i)) + } + } + let find_tag_end = fn(start : Int) -> Int { + for i = start; i < chars.length(); i = i + 1 { + if chars.unsafe_get(i) == '>' { + return i + } + } + -1 + } + for i = 0; i < chars.length(); { + if chars.unsafe_get(i) != '<' { + buffer.write_char(chars.unsafe_get(i)) + continue i + 1 + } + let end = find_tag_end(i + 1) + if end == -1 { + buffer.write_char(chars[i]) + continue i + 1 + } + let tag = input[i + 1:end].to_owned() + if tag == "/" { + if stack.length() > 1 { + flush() + ignore(stack.pop()) + } else { + append_raw(i, end + 1) + } + continue end + 1 + } + if tag.has_prefix("/") { + let close_tag = tag[1:].to_owned() + if close_tag != "" && has_named_style_frame(stack, close_tag) { + flush() + ignore(pop_named_style_frame(stack, close_tag)) + } else { + append_raw(i, end + 1) + } + continue end + 1 + } + match apply_inline_tag(current_style(), tag, formatter) { + Some(next_style) => { + flush() + stack.push({ tag: normalize_style_tag_name(tag), style: next_style }) + } + None => append_raw(i, end + 1) + } + continue end + 1 + } + flush() + segments +} + +fn render_styled_text(text : String, formatter : TextFormatter, mode : StyleMarkupMode) -> String { + match mode { + StyleMarkupMode::Disabled => return text + _ => () + } + let enabled = use_ansi_color(formatter.color_mode) + let scoped = formatter.with_style_markup(mode) + let segments = parse_inline_markup(text, scoped) + let out = StringBuilder::new() + for segment in segments { + out.write_string(ansi_wrap_with_style(segment.text, segment.style, enabled)) + } + out.to_string() +} + +fn render_inline_markup(message : String, formatter : TextFormatter) -> String { + render_styled_text(message, formatter, formatter.style_markup) +} + +fn level_ansi_code(level : @core.Level) -> String { + match level { + @core.Level::Trace => "90" + @core.Level::Debug => "36" + @core.Level::Info => "32" + @core.Level::Warn => "33" + @core.Level::Error => "31;1" + } +} + +fn fields_to_json(fields : Array[@core.Field]) -> Json { + let obj : Map[String, Json] = {} + for item in fields { + obj[item.key] = Json::string(item.value) + } + Json::object(obj) +} + +fn timestamp_text(rec : @core.Record, formatter : TextFormatter) -> String { + if formatter.show_timestamp && rec.timestamp_ms != 0UL { + ansi_wrap(rec.timestamp_ms.to_string(), "90", use_ansi_color(formatter.color_mode)) + } else { + "" + } +} + +fn level_text(rec : @core.Record, formatter : TextFormatter) -> String { + if formatter.show_level { + ansi_wrap(rec.level.label(), level_ansi_code(rec.level), use_ansi_color(formatter.color_mode)) + } else { + "" + } +} + +fn target_text(rec : @core.Record, formatter : TextFormatter) -> String { + if formatter.show_target && rec.target != "" { + let rendered = render_styled_text(rec.target, formatter, formatter.target_style_markup) + ansi_wrap(rendered, "34", use_ansi_color(formatter.color_mode)) + } else { + "" + } +} + +fn format_field_text(field : @core.Field, formatter : TextFormatter) -> String { + let value = render_styled_text(field.value, formatter, formatter.fields_style_markup) + "\{field.key}=\{value}" +} + +fn fields_text(rec : @core.Record, formatter : TextFormatter) -> String { + if formatter.show_fields && rec.fields.length() != 0 { + let content = rec.fields.map(fn(field) { format_field_text(field, formatter) }).join(formatter.field_separator) + ansi_wrap(content, "35", use_ansi_color(formatter.color_mode)) + } else { + "" + } +} + +fn render_template(rec : @core.Record, formatter : TextFormatter) -> String { + formatter.template + .replace_all(old="{timestamp}", new=timestamp_text(rec, formatter)) + .replace_all(old="{timestamp_ms}", new=timestamp_text(rec, formatter)) + .replace_all(old="{level}", new=level_text(rec, formatter)) + .replace_all(old="{target}", new=target_text(rec, formatter)) + .replace_all(old="{message}", new=render_inline_markup(rec.message, formatter)) + .replace_all(old="{fields}", new=fields_text(rec, formatter)) + .trim() + .to_owned() +} + +pub fn format_text(rec : @core.Record, formatter~ : TextFormatter = text_formatter()) -> String { + if formatter.template != "" { + return render_template(rec, formatter) + } + let parts : Array[String] = [] + if formatter.show_timestamp && rec.timestamp_ms != 0UL { + parts.push("[\{timestamp_text(rec, formatter)}]") + } + if formatter.show_level { + parts.push("[\{level_text(rec, formatter)}]") + } + if formatter.show_target && rec.target != "" { + parts.push("[\{target_text(rec, formatter)}]") + } + parts.push(render_inline_markup(rec.message, formatter)) + let base = parts.join(formatter.separator) + if !formatter.show_fields || rec.fields.length() == 0 { + base + } else { + let details = fields_text(rec, formatter) + "\{base}\{formatter.separator}\{details}" + } +} + +pub fn format_json(rec : @core.Record) -> String { + let obj : Map[String, Json] = { + "level": Json::string(rec.level.label()), + "message": Json::string(rec.message), + "fields": fields_to_json(rec.fields), + } + if rec.timestamp_ms != 0UL { + obj["timestamp_ms"] = rec.timestamp_ms.to_json() + } + if rec.target == "" { + Json::object(obj).stringify() + } else { + obj["target"] = Json::string(rec.target) + Json::object(obj).stringify() + } +} diff --git a/src/utils/moon.pkg b/src/utils/moon.pkg index ea28217..f1c50a1 100644 --- a/src/utils/moon.pkg +++ b/src/utils/moon.pkg @@ -1,3 +1,6 @@ import { "Nanaloveyuki/BitLogger/src/core" @core, + "moonbitlang/core/env" @env, + "moonbitlang/core/json", + "moonbitlang/core/ref", }