diff --git a/README.md b/README.md index e508bbb..4d061d6 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,20 @@ logger.info("styled output and alert") +
关闭 style markup 解析示例 + +```moonbit +let formatter = text_formatter( + color_mode=ColorMode::Always, +).without_style_markup() + +let logger = Logger::new(text_console_sink(formatter), target="raw") + +logger.info("kept as raw text") +``` + +
+
JSON 配置加载示例 ```moonbit @@ -226,6 +240,20 @@ logger.info("styled from json")
+
JSON style_markup 模式示例 + +```moonbit +let config = parse_logger_config_text( + "{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_markup\":\"disabled\"}}}", +) + +let logger = build_logger(config) + +logger.info("still raw") +``` + +
+
native 文件 sink 示例 ```moonbit @@ -277,6 +305,7 @@ match logger.file_runtime_state() { - `QueueConfig`, `TextFormatterConfig`, `SinkConfig` 可分别通过 `queue_config_to_json(...)` / `stringify_queue_config(...)`, `text_formatter_config_to_json(...)` / `stringify_text_formatter_config(...)`, `sink_config_to_json(...)` / `stringify_sink_config(...)` 单独导出 JSON - 支持字段: `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.rotation`, `sink.text_formatter`, `queue` - `TextFormatter` / `TextFormatterConfig` 提供 `color_mode = Never | Auto | Always`, 可控制 ANSI 文本着色 +- `TextFormatter` / `TextFormatterConfig` 提供 `style_markup = disabled | builtin | full`, 可决定是否解析 style markup 以及是否启用 custom style tag - `message` 支持轻量 inline style tag: `...`, `...`, `<#ff0000>...`, `...` - 运行期样式标签 API: `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, `default_style_tag_registry()`, `set_tag(...)`, `define_alias(...)` - 样式标签优先级: formatter 局部 `style_tags` > 全局 style tag registry > 内置标签 @@ -306,6 +335,7 @@ match logger.file_runtime_state() { - `file_sink_state_to_json(...)`, `stringify_file_sink_state(...)`, `runtime_file_state_to_json(...)`, `stringify_runtime_file_state(...)` 可直接把 file / queued-file 快照导出为 JSON, 便于排障或上报 - `sink.text_formatter.template` 支持固定 token: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, `{fields}` - `sink.text_formatter.color_mode` 支持 `never`, `auto`, `always` +- `sink.text_formatter.style_markup` 支持 `disabled`, `builtin`, `full` - `sink.text_formatter.style_tags.` 支持 `fg`, `bg`, `bold`, `dim`, `italic`, `underline` - 可由配置直接组装的 sink 类型: `console`, `json_console`, `text_console`, `file` - `queue` 作为显式包装层附着在最终 sink 外侧. 这仍然是同步 drain 模型, 不是 async runtime diff --git a/bitlogger/BitLogger_test.mbt b/bitlogger/BitLogger_test.mbt index c5ce637..848cbf3 100644 --- a/bitlogger/BitLogger_test.mbt +++ b/bitlogger/BitLogger_test.mbt @@ -74,8 +74,9 @@ test "logger config parser reads formatter and queue options" { test "logger config parser reads formatter style tags" { let config = parse_logger_config_text( - "{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_tags\":{\"accent\":{\"fg\":\"#4cc9f0\",\"bold\":true},\"panel\":{\"bg\":\"#202020\",\"underline\":true}}}}}", + "{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_markup\":\"builtin\",\"style_tags\":{\"accent\":{\"fg\":\"#4cc9f0\",\"bold\":true},\"panel\":{\"bg\":\"#202020\",\"underline\":true}}}}}", ) + inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="builtin") inspect(config.sink.text_formatter.style_tags.length(), content="2") match config.sink.text_formatter.style_tags.get("accent") { Some(style) => { @@ -148,6 +149,7 @@ test "logger config stringify roundtrips formatter style tags" { kind=SinkKind::TextConsole, text_formatter=TextFormatterConfig::new( color_mode=ColorMode::Always, + style_markup=StyleMarkupMode::Builtin, style_tags={ "accent": text_style(fg=Some("#4cc9f0"), bold=true), "panel": text_style(bg=Some("#202020"), dim=true), @@ -158,6 +160,7 @@ test "logger config stringify roundtrips formatter style tags" { ) let config = parse_logger_config_text(text) inspect(color_mode_label(config.sink.text_formatter.color_mode), content="always") + inspect(style_markup_mode_label(config.sink.text_formatter.style_markup), content="builtin") inspect(config.sink.text_formatter.style_tags.length(), content="2") inspect(config.sink.text_formatter.style_tags.get("accent").unwrap().fg.unwrap(), content="#4cc9f0") inspect(config.sink.text_formatter.style_tags.get("accent").unwrap().bold, content="true") @@ -204,12 +207,13 @@ test "config subtype json helpers stringify stable shapes" { field_separator=",", template="[{level}] {message} :: {fields}", color_mode=ColorMode::Always, + style_markup=StyleMarkupMode::Builtin, style_tags={ "accent": text_style(fg=Some("#4cc9f0"), bold=true), }, ), ), - content="{\"show_timestamp\":false,\"show_level\":true,\"show_target\":false,\"show_fields\":true,\"separator\":\" | \",\"field_separator\":\",\",\"template\":\"[{level}] {message} :: {fields}\",\"color_mode\":\"always\",\"style_tags\":{\"accent\":{\"bold\":true,\"dim\":false,\"italic\":false,\"underline\":false,\"fg\":\"#4cc9f0\"}}}", + content="{\"show_timestamp\":false,\"show_level\":true,\"show_target\":false,\"show_fields\":true,\"separator\":\" | \",\"field_separator\":\",\",\"template\":\"[{level}] {message} :: {fields}\",\"color_mode\":\"always\",\"style_markup\":\"builtin\",\"style_tags\":{\"accent\":{\"bold\":true,\"dim\":false,\"italic\":false,\"underline\":false,\"fg\":\"#4cc9f0\"}}}", ) inspect( stringify_sink_config( @@ -222,7 +226,7 @@ test "config subtype json helpers stringify stable shapes" { text_formatter=TextFormatterConfig::new(show_timestamp=false, color_mode=ColorMode::Auto), ), ), - content="{\"kind\":\"file\",\"path\":\"demo.log\",\"append\":false,\"auto_flush\":false,\"text_formatter\":{\"show_timestamp\":false,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"auto\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}", + content="{\"kind\":\"file\",\"path\":\"demo.log\",\"append\":false,\"auto_flush\":false,\"text_formatter\":{\"show_timestamp\":false,\"show_level\":true,\"show_target\":true,\"show_fields\":true,\"separator\":\" \",\"field_separator\":\" \",\"template\":\"\",\"color_mode\":\"auto\",\"style_markup\":\"full\"},\"rotation\":{\"max_bytes\":128,\"max_backups\":2}}", ) } @@ -242,6 +246,40 @@ test "config formatter style tags render in built logger" { inspect(rendered, content="[\u{001b}[32mINFO\u{001b}[0m] \u{001b}[38;2;76;201;240;1mtag\u{001b}[0m") } +test "config builtin style markup ignores custom tags" { + let formatter = TextFormatterConfig::new( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + style_markup=StyleMarkupMode::Builtin, + style_tags={ + "accent": text_style(fg=Some("#4cc9f0"), bold=true), + }, + ) + let rendered = format_text( + Record::new(Level::Info, "custom builtin"), + formatter=formatter.to_formatter(), + ) + inspect(rendered, content="custom \u{001b}[31mbuiltin\u{001b}[0m") +} + +test "config disabled style markup keeps raw tags" { + let formatter = TextFormatterConfig::new( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + style_markup=StyleMarkupMode::Disabled, + style_tags={ + "accent": text_style(fg=Some("#4cc9f0"), bold=true), + }, + ) + let rendered = format_text( + Record::new(Level::Info, "raw tag"), + formatter=formatter.to_formatter(), + ) + inspect(rendered, content="raw tag") +} + test "build logger from config supports queued text console" { let logger = build_logger( LoggerConfig::new( diff --git a/bitlogger/BitLogger_wbtest.mbt b/bitlogger/BitLogger_wbtest.mbt index 7f728cb..3df1c00 100644 --- a/bitlogger/BitLogger_wbtest.mbt +++ b/bitlogger/BitLogger_wbtest.mbt @@ -123,6 +123,39 @@ test "text formatter keeps unknown inline tags as plain text" { ) } +test "text formatter can disable style markup parsing" { + let rec = record(Level::Info, "boom bold", target="svc") + inspect( + format_text( + rec, + formatter=text_formatter(color_mode=ColorMode::Always).without_style_markup(), + ), + content="[\u{001b}[32mINFO\u{001b}[0m] [\u{001b}[34msvc\u{001b}[0m] boom bold", + ) +} + +test "text formatter builtin style markup mode ignores custom tags" { + let formatter = text_formatter( + show_level=false, + show_target=false, + color_mode=ColorMode::Always, + style_markup=StyleMarkupMode::Builtin, + ).with_style_tags( + style_tag_registry().set_tag("accent", fg=Some("#4cc9f0"), bold=true), + ) + let rec = record(Level::Info, "custom builtin") + inspect( + format_text(rec, formatter=formatter), + content="custom \u{001b}[31mbuiltin\u{001b}[0m", + ) +} + +test "style markup mode label is stable" { + inspect(style_markup_mode_label(StyleMarkupMode::Disabled), content="disabled") + inspect(style_markup_mode_label(StyleMarkupMode::Builtin), content="builtin") + inspect(style_markup_mode_label(StyleMarkupMode::Full), content="full") +} + test "text formatter supports custom style tags" { let formatter = text_formatter( show_level=false, diff --git a/bitlogger/README.mbt.md b/bitlogger/README.mbt.md index 0fc3472..fafb3cb 100644 --- a/bitlogger/README.mbt.md +++ b/bitlogger/README.mbt.md @@ -206,6 +206,16 @@ test { } ``` +```mbt check +test { + let formatter = text_formatter( + color_mode=ColorMode::Always, + ).without_style_markup() + let logger = Logger::new(text_console_sink(formatter), target="raw") + logger.info("kept as raw text") +} +``` + ```mbt check test { let config = parse_logger_config_text( @@ -227,10 +237,21 @@ test { } ``` +```mbt check +test { + let config = parse_logger_config_text( + "{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_markup\":\"disabled\"}}}", + ) + let logger = build_logger(config) + logger.info("still raw") +} +``` + ## Formatter Template / 模板格式 - supported tokens / 支持的 token: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, `{fields}` - `color_mode` / `color_mode`: `never`, `auto`, `always` +- `style_markup` / `style_markup`: `disabled`, `builtin`, `full` - inline style tags / inline 样式标签: `...`, `...`, `<#ff0000>...`, `...` - runtime style tags / 运行期样式标签: `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, `default_style_tag_registry()`, `set_tag(...)`, `define_alias(...)` - style tag priority / 标签优先级: formatter local `style_tags` > global style tag registry > builtin tags diff --git a/bitlogger/config.mbt b/bitlogger/config.mbt index 6999be0..55e2e60 100644 --- a/bitlogger/config.mbt +++ b/bitlogger/config.mbt @@ -18,6 +18,7 @@ pub struct TextFormatterConfig { field_separator : String template : String color_mode : ColorMode + style_markup : StyleMarkupMode style_tags : Map[String, TextStyle] } @@ -30,6 +31,7 @@ pub fn TextFormatterConfig::new( field_separator~ : String = " ", template~ : String = "", color_mode~ : ColorMode = ColorMode::Never, + style_markup~ : StyleMarkupMode = StyleMarkupMode::Full, style_tags~ : Map[String, TextStyle] = {}, ) -> TextFormatterConfig { { @@ -41,6 +43,7 @@ pub fn TextFormatterConfig::new( field_separator, template, color_mode, + style_markup, style_tags, } } @@ -63,6 +66,7 @@ pub fn TextFormatterConfig::to_formatter(self : TextFormatterConfig) -> TextForm field_separator=self.field_separator, template=self.template, color_mode=self.color_mode, + style_markup=self.style_markup, style_tags=if self.style_tags.length() == 0 { None } else { @@ -857,6 +861,15 @@ fn parse_color_mode(name : String) -> ColorMode raise ConfigError { } } +fn parse_style_markup_mode(name : String) -> StyleMarkupMode raise ConfigError { + match name.to_upper() { + "DISABLED" => StyleMarkupMode::Disabled + "BUILTIN" => StyleMarkupMode::Builtin + "FULL" => StyleMarkupMode::Full + _ => raise ConfigError::InvalidConfig("Unsupported style markup mode: " + name) + } +} + fn sink_kind_label(kind : SinkKind) -> String { match kind { SinkKind::Console => "console" @@ -877,6 +890,7 @@ fn parse_text_formatter_config(value : @json_parser.JsonValue) -> TextFormatterC field_separator=get_string(obj, "field_separator", default=" "), template=get_string(obj, "template", default=""), color_mode=parse_color_mode(get_string(obj, "color_mode", default="never")), + style_markup=parse_style_markup_mode(get_string(obj, "style_markup", default="full")), style_tags=match obj.get("style_tags") { None => {} Some(inner) => parse_style_tags_config(inner) @@ -1000,6 +1014,7 @@ pub fn text_formatter_config_to_json(config : TextFormatterConfig) -> @json_pars "field_separator": @json_parser.JsonValue::String(config.field_separator), "template": @json_parser.JsonValue::String(config.template), "color_mode": @json_parser.JsonValue::String(color_mode_label(config.color_mode)), + "style_markup": @json_parser.JsonValue::String(style_markup_mode_label(config.style_markup)), } if config.style_tags.length() != 0 { obj["style_tags"] = style_tags_config_to_json(config.style_tags) diff --git a/bitlogger/formatter.mbt b/bitlogger/formatter.mbt index 5f6421c..034b04a 100644 --- a/bitlogger/formatter.mbt +++ b/bitlogger/formatter.mbt @@ -6,6 +6,12 @@ pub(all) enum ColorMode { Always } +pub(all) enum StyleMarkupMode { + Disabled + Builtin + Full +} + pub struct TextStyle { fg : String? bg : String? @@ -165,6 +171,7 @@ pub struct TextFormatter { field_separator : String template : String color_mode : ColorMode + style_markup : StyleMarkupMode style_tags : StyleTagRegistry? } @@ -177,6 +184,7 @@ pub fn text_formatter( field_separator~ : String = " ", template~ : String = "", color_mode~ : ColorMode = ColorMode::Never, + style_markup~ : StyleMarkupMode = StyleMarkupMode::Full, style_tags~ : StyleTagRegistry? = None, ) -> TextFormatter { { @@ -188,10 +196,27 @@ pub fn text_formatter( field_separator, template, color_mode, + style_markup, 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 { + { ..self, style_markup } +} + +pub fn TextFormatter::without_style_markup(self : TextFormatter) -> TextFormatter { + { ..self, style_markup: StyleMarkupMode::Disabled } +} + pub fn TextFormatter::with_style_tags(self : TextFormatter, style_tags : StyleTagRegistry) -> TextFormatter { { ..self, style_tags: Some(style_tags) } } @@ -427,6 +452,10 @@ fn builtin_text_style_for_tag(tag : String) -> TextStyle? { 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) @@ -522,6 +551,10 @@ fn parse_inline_markup(input : String, formatter : TextFormatter) -> Array[Style } fn render_inline_markup(message : String, formatter : TextFormatter) -> String { + match formatter.style_markup { + StyleMarkupMode::Disabled => return message + _ => () + } let enabled = use_ansi_color(formatter.color_mode) let segments = parse_inline_markup(message, formatter) let out = StringBuilder::new() diff --git a/docs/README-en.md b/docs/README-en.md index 8c35bcb..52d0f2a 100644 --- a/docs/README-en.md +++ b/docs/README-en.md @@ -183,6 +183,18 @@ let logger = Logger::new(text_console_sink(formatter), target="styled") logger.info("styled output and alert") ``` +Disable style markup parsing: + +```moonbit +let formatter = text_formatter( + color_mode=ColorMode::Always, +).without_style_markup() + +let logger = Logger::new(text_console_sink(formatter), target="raw") + +logger.info("kept as raw text") +``` + JSON config loading: ```moonbit @@ -208,6 +220,18 @@ let logger = build_logger(config) logger.info("styled from json") ``` +JSON `style_markup` mode: + +```moonbit +let config = parse_logger_config_text( + "{\"sink\":{\"kind\":\"text_console\",\"text_formatter\":{\"color_mode\":\"always\",\"style_markup\":\"disabled\"}}}", +) + +let logger = build_logger(config) + +logger.info("still raw") +``` + Native file sink: ```moonbit @@ -257,6 +281,7 @@ match logger.file_runtime_state() { - `QueueConfig`, `TextFormatterConfig`, and `SinkConfig` can also be exported independently through `queue_config_to_json(...)` / `stringify_queue_config(...)`, `text_formatter_config_to_json(...)` / `stringify_text_formatter_config(...)`, and `sink_config_to_json(...)` / `stringify_sink_config(...)`. - Supported keys include `min_level`, `target`, `timestamp`, `sink.kind`, `sink.path`, `sink.append`, `sink.auto_flush`, `sink.rotation`, `sink.text_formatter`, and `queue`. - `TextFormatter` and `TextFormatterConfig` now include `color_mode = Never | Auto | Always` for ANSI text coloring control. +- `TextFormatter` and `TextFormatterConfig` also include `style_markup = disabled | builtin | full` so callers can choose whether style markup is parsed and whether custom tags are active. - `message` also supports lightweight inline style tags such as `...`, `...`, `<#ff0000>...`, and `...`. - Runtime style-tag APIs now include `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, `default_style_tag_registry()`, `set_tag(...)`, and `define_alias(...)`. - Style-tag lookup priority is formatter-local `style_tags` > global style tag registry > builtin tags. @@ -286,6 +311,7 @@ match logger.file_runtime_state() { - `file_sink_state_to_json(...)`, `stringify_file_sink_state(...)`, `runtime_file_state_to_json(...)`, and `stringify_runtime_file_state(...)` can export file and queued-file snapshots directly as JSON for diagnostics or reporting. - `sink.text_formatter.template` currently supports fixed tokens: `{timestamp}`, `{timestamp_ms}`, `{level}`, `{target}`, `{message}`, and `{fields}`. - `sink.text_formatter.color_mode` currently supports `never`, `auto`, and `always`. +- `sink.text_formatter.style_markup` currently supports `disabled`, `builtin`, and `full`. - `sink.text_formatter.style_tags.` currently supports `fg`, `bg`, `bold`, `dim`, `italic`, and `underline`. - Config-driven sink assembly currently supports `console`, `json_console`, `text_console`, and `file`. - `queue` remains a synchronous bounded wrapper around the final sink, not an async runtime. diff --git a/docs/changes/0.4.0.md b/docs/changes/0.4.0.md index 386919f..a41384d 100644 --- a/docs/changes/0.4.0.md +++ b/docs/changes/0.4.0.md @@ -14,6 +14,8 @@ version 0.4.0 - feat: add `TextStyle`, `StyleTagRegistry`, `style_tag_registry()`, and `default_style_tag_registry()` for reusable inline style tags - feat: add formatter-local `style_tags`, global style tag registry helpers, builtin-tag override support, and alias reuse via `define_alias(...)` - feat: support minimal `sink.text_formatter.style_tags` JSON config parsing and serialization for custom formatter tag styles +- feat: add `StyleMarkupMode = Disabled | Builtin | Full` plus formatter helpers so callers can explicitly disable style parsing or allow builtin-only parsing +- feat: support `sink.text_formatter.style_markup` in JSON config parsing and serialization ### Test @@ -24,11 +26,13 @@ version 0.4.0 - test: cover plain mode tag stripping, nested tags, hex tags, and unknown-tag fallback behavior - test: cover custom tags, builtin-tag override, formatter-vs-global priority, global registry fallback, and alias reuse - test: cover formatter `style_tags` JSON parsing, config roundtrip, JSON helper export, and config-driven styled formatter rendering +- test: cover disabled markup mode, builtin-only mode, and config-driven `style_markup` behavior ### Example - docs: add `color_mode` usage examples to formatter documentation - docs: add runtime and JSON `style_tags` examples, and document current JSON schema scope +- docs: add runtime and JSON examples for toggling style markup parsing ### Notes @@ -37,3 +41,4 @@ version 0.4.0 - unknown or invalid inline tags currently fall back to plain text and do not raise formatter errors - formatter-local tag lookup currently takes precedence over global tag lookup, and global lookup takes precedence over builtin tags - JSON config currently supports concrete style objects only; alias-style declarations remain runtime-only +- users can now decide whether custom style parsing is enabled through runtime formatter APIs or `sink.text_formatter.style_markup`