From db442d2fc5b968f6c18ebcd19ead3a3b8e5d6f1c Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 13:40:04 +0100 Subject: [PATCH 01/15] Add pretty-print JSON support to RichTextBox sink Introduces pretty-printing for JSON output in the RichTextBox sink, controlled via new options. --- CODE_OF_CONDUCT.md | 2 +- Demo/Form1.Designer.cs | 14 +- Demo/Form1.cs | 17 +- README.md | 9 +- SECURITY.md | 2 +- .../Integration/JsonFormattingTests.cs | 161 ++++++++ .../RichTextBoxSinkTestBase.cs | 8 + ...extBoxSinkLoggerConfigurationExtensions.cs | 25 +- .../Formatting/DisplayValueFormatter.cs | 14 +- .../Formatting/JsonValueFormatter.cs | 349 +++++++++++++----- .../Formatting/ValueFormatter.cs | 7 +- .../Formatting/ValueFormatterState.cs | 35 +- .../Rendering/MessageTemplateTokenRenderer.cs | 7 +- .../Rendering/PropertiesTokenRenderer.cs | 7 +- .../Rendering/TemplateRenderer.cs | 7 +- .../Sinks/RichTextBoxForms/RichTextBoxSink.cs | 2 +- .../RichTextBoxSinkOptions.cs | 27 +- 17 files changed, 577 insertions(+), 116 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 87ef7e4..832b495 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -55,7 +55,7 @@ a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at `simon.vonhoff[at]outlook.com`. All +reported by contacting the project team at `simon.vonhoff@outlook.com`. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. diff --git a/Demo/Form1.Designer.cs b/Demo/Form1.Designer.cs index 049794b..890a237 100644 --- a/Demo/Form1.Designer.cs +++ b/Demo/Form1.Designer.cs @@ -50,6 +50,7 @@ private void InitializeComponent() this.btnReset = new System.Windows.Forms.ToolStripButton(); this.toolStripSeparator6 = new System.Windows.Forms.ToolStripSeparator(); this.btnAutoScroll = new System.Windows.Forms.ToolStripButton(); + this.btnPrettyPrint = new System.Windows.Forms.ToolStripButton(); this.panel1 = new System.Windows.Forms.Panel(); this.richTextBox1 = new System.Windows.Forms.RichTextBox(); this.btnRestore = new System.Windows.Forms.ToolStripButton(); @@ -174,7 +175,8 @@ private void InitializeComponent() this.btnDispose, this.btnReset, this.toolStripSeparator6, - this.btnAutoScroll}); + this.btnAutoScroll, + this.btnPrettyPrint}); this.toolStrip2.Location = new System.Drawing.Point(0, 25); this.toolStrip2.Name = "toolStrip2"; this.toolStrip2.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0); @@ -269,6 +271,15 @@ private void InitializeComponent() this.btnAutoScroll.ToolTipText = "Toggle auto-scroll behavior"; this.btnAutoScroll.Click += new System.EventHandler(this.btnAutoScroll_Click); // + // btnPrettyPrint + // + this.btnPrettyPrint.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; + this.btnPrettyPrint.Name = "btnPrettyPrint"; + this.btnPrettyPrint.Size = new System.Drawing.Size(100, 22); + this.btnPrettyPrint.Text = "Enable Pretty Print"; + this.btnPrettyPrint.ToolTipText = "Toggle pretty-printed JSON formatting"; + this.btnPrettyPrint.Click += new System.EventHandler(this.btnPrettyPrint_Click); + // // panel1 // this.panel1.Controls.Add(this.richTextBox1); @@ -363,6 +374,7 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripButton btnReset; private System.Windows.Forms.ToolStripSeparator toolStripSeparator6; private System.Windows.Forms.ToolStripButton btnAutoScroll; + private System.Windows.Forms.ToolStripButton btnPrettyPrint; private System.Windows.Forms.ToolStripButton btnRestore; private System.Windows.Forms.ToolStripButton btnClear; private System.Windows.Forms.ToolStripSeparator toolStripSeparator2; diff --git a/Demo/Form1.cs b/Demo/Form1.cs index a0a9574..4edfb45 100644 --- a/Demo/Form1.cs +++ b/Demo/Form1.cs @@ -35,6 +35,7 @@ public partial class Form1 : Form private RichTextBoxSinkOptions? _options; private RichTextBoxSink? _sink; private bool _toolbarsVisible = true; + private bool _prettyPrintJson = false; public Form1() { @@ -47,7 +48,8 @@ private void Initialize() _options = new RichTextBoxSinkOptions( theme: ThemePresets.Literate, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}", - formatProvider: new CultureInfo("en-US")); + formatProvider: new CultureInfo("en-US"), + prettyPrintJson: _prettyPrintJson); _sink = new RichTextBoxSink(richTextBox1, _options); Log.Logger = new LoggerConfiguration() @@ -77,6 +79,7 @@ private void Initialize() Log.Debug("Started logger."); btnDispose.Enabled = true; + btnPrettyPrint.Text = _prettyPrintJson ? "Disable Pretty Print" : "Enable Pretty Print"; } private void Form1_Load(object sender, EventArgs e) @@ -350,6 +353,18 @@ private void btnAutoScroll_Click(object sender, EventArgs e) btnAutoScroll.Text = _options.AutoScroll ? "Disable Auto Scroll" : "Enable Auto Scroll"; } + private void btnPrettyPrint_Click(object sender, EventArgs e) + { + _prettyPrintJson = !_prettyPrintJson; + btnPrettyPrint.Text = _prettyPrintJson ? "Disable Pretty Print" : "Enable Pretty Print"; + + // Recreate the sink and logger with new pretty print setting + CloseAndFlush(); + Initialize(); + + Log.Information("Pretty print JSON: {PrettyPrint}", _prettyPrintJson); + } + private void Form1_KeyDown(object sender, KeyEventArgs e) { if (e.Control && e.KeyCode == Keys.T) diff --git a/README.md b/README.md index 8067908..1c70275 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,16 @@ A [Serilog](https://github.com/serilog/serilog) sink that writes log events to a - Multiple theme presets with customization options - High-performance asynchronous processing - Line limit to control memory usage +- Pretty printing of JSON objects - WCAG compliant color schemes based on the [Serilog WPF RichTextBox](https://github.com/serilog-contrib/serilog-sinks-richtextbox) sink. -## Get Started +> **Consider supporting the development of this project on Ko-fi!** +> +> Your support keeps this project alive. Even a small gesture means a lot. +> +> [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/vonhoff) + +## Getting Started Install the package from NuGet: diff --git a/SECURITY.md b/SECURITY.md index bebcfc1..0ccb2ef 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Reporting a Vulnerability -If you find a security issue, please email `simon.vonhoff[at]outlook.com` with a description. Include a suggested fix if you have one. +If you find a security issue, please email `simon.vonhoff@outlook.com` with a description. Include a suggested fix if you have one. We will review the report, release a fix or mitigation if needed, and credit you. Do **not** disclose the issue publicly until a fix is released. Once addressed, public disclosure is allowed. diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs index 6b3b257..3baacc5 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs @@ -1,4 +1,7 @@ +using System; using Serilog.Events; +using Serilog.Sinks.RichTextBoxForms; +using Serilog.Sinks.RichTextBoxForms.Themes; using Xunit; namespace Serilog.Tests.Integration @@ -92,5 +95,163 @@ public void ControlCharacters_AreEscapedAsUnicode() Assert.Equal(expected, RenderAndGetText(strEvent, "{Message:j}")); } } + + [Fact] + public void PrettyPrintJson_FormatsNestedObjectsWithIndentation() + { + var nestedProp = new LogEventProperty("Nested", new StructureValue(new[] + { + new LogEventProperty("Id", new ScalarValue(123)), + new LogEventProperty("Name", new ScalarValue("test")) + }, "MyObj")); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Nested:j}"), new[] { nestedProp }); + + var options = new RichTextBoxSinkOptions( + theme: _defaultTheme, + prettyPrintJson: true, + indentSize: 4, + useSpacesForIndent: true); + + var result = RenderAndGetText(logEvent, "{Message:l}", options); + var expected = "{\n \"Id\": 123,\n \"Name\": \"test\",\n \"$type\": \"MyObj\"\n}"; + Assert.Equal(expected, result); + } + + [Fact] + public void PrettyPrintJson_FormatsArraysWithIndentation() + { + var arrayProp = new LogEventProperty("Array", new SequenceValue(new[] + { + new ScalarValue(1), + new ScalarValue(2), + new ScalarValue(3) + })); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Array:j}"), new[] { arrayProp }); + + var options = new RichTextBoxSinkOptions( + theme: _defaultTheme, + prettyPrintJson: true, + indentSize: 4, + useSpacesForIndent: true); + + var result = RenderAndGetText(logEvent, "{Message:l}", options); + var expected = "[\n 1,\n 2,\n 3\n]"; + Assert.Equal(expected, result); + } + + [Fact] + public void PrettyPrintJson_FormatsDictionariesWithIndentation() + { + var dict = new Dictionary + { + { new ScalarValue("a"), new ScalarValue(1) }, + { new ScalarValue("b"), new ScalarValue("hello") } + }; + var dictProp = new LogEventProperty("DictProp", new DictionaryValue(dict)); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{DictProp:j}"), new[] { dictProp }); + + var options = new RichTextBoxSinkOptions( + theme: _defaultTheme, + prettyPrintJson: true, + indentSize: 4, + useSpacesForIndent: true); + + var result = RenderAndGetText(logEvent, "{Message:l}", options); + var expected = "{\n \"a\": 1,\n \"b\": \"hello\"\n}"; + Assert.Equal(expected, result); + } + + [Fact] + public void PrettyPrintJson_FormatsNestedStructures() + { + var inner = new StructureValue(new[] + { + new LogEventProperty("Value", new ScalarValue(42)) + }, "Inner"); + var outer = new LogEventProperty("Outer", new StructureValue(new[] + { + new LogEventProperty("Inner", inner), + new LogEventProperty("Name", new ScalarValue("test")) + }, "Outer")); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Outer:j}"), new[] { outer }); + + var options = new RichTextBoxSinkOptions( + theme: _defaultTheme, + prettyPrintJson: true, + indentSize: 4, + useSpacesForIndent: true); + + var result = RenderAndGetText(logEvent, "{Message:l}", options); + var expected = "{\n \"Inner\": {\n \"Value\": 42,\n \"$type\": \"Inner\"\n },\n \"Name\": \"test\",\n \"$type\": \"Outer\"\n}"; + Assert.Equal(expected, result); + } + + [Fact] + public void PrettyPrintJson_EmptyCollectionsFormatCorrectly() + { + var emptyArray = new LogEventProperty("EmptyArray", new SequenceValue(Array.Empty())); + var emptyObject = new LogEventProperty("EmptyObject", new StructureValue(Array.Empty(), null)); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("Array: {EmptyArray:j}, Object: {EmptyObject:j}"), new[] { emptyArray, emptyObject }); + + var options = new RichTextBoxSinkOptions( + theme: _defaultTheme, + prettyPrintJson: true, + indentSize: 4, + useSpacesForIndent: true); + + var result = RenderAndGetText(logEvent, "{Message:l}", options); + Assert.Contains("Array: []", result); + Assert.Contains("Object: {}", result); + } + + [Fact] + public void PrettyPrintJson_UsesTabsWhenConfigured() + { + var prop = new LogEventProperty("Test", new StructureValue(new[] + { + new LogEventProperty("Id", new ScalarValue(123)) + }, "MyObj")); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Test:j}"), new[] { prop }); + + var options = new RichTextBoxSinkOptions( + theme: _defaultTheme, + prettyPrintJson: true, + indentSize: 4, + useSpacesForIndent: false); + + var result = RenderAndGetText(logEvent, "{Message:l}", options); + var expected = "{\n\t\"Id\": 123,\n\t\"$type\": \"MyObj\"\n}"; + Assert.Equal(expected, result); + } + + [Fact] + public void PrettyPrintJson_RespectsIndentSize() + { + var prop = new LogEventProperty("Test", new StructureValue(new[] + { + new LogEventProperty("Id", new ScalarValue(123)) + }, "MyObj")); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Test:j}"), new[] { prop }); + + var options = new RichTextBoxSinkOptions( + theme: _defaultTheme, + prettyPrintJson: true, + indentSize: 2, + useSpacesForIndent: true); + + var result = RenderAndGetText(logEvent, "{Message:l}", options); + var expected = "{\n \"Id\": 123,\n \"$type\": \"MyObj\"\n}"; + Assert.Equal(expected, result); + } + + [Fact] + public void CompactJson_StillWorksByDefault() + { + var complexProp = new LogEventProperty("ComplexProp", new StructureValue(new[] { new LogEventProperty("Id", new ScalarValue(123)) }, "MyObj")); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("Value: {ComplexProp}"), new[] { complexProp }); + + // Default behavior (compact) + Assert.Equal("Value: {\"Id\": 123, \"$type\": \"MyObj\"}", RenderAndGetText(logEvent, "{Message:j}")); + } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/RichTextBoxSinkTestBase.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/RichTextBoxSinkTestBase.cs index 28e6efd..234c65d 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/RichTextBoxSinkTestBase.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/RichTextBoxSinkTestBase.cs @@ -45,6 +45,14 @@ protected string RenderAndGetText(LogEvent logEvent, string outputTemplate, IFor return _richTextBox.Text.TrimEnd('\n', '\r'); } + protected string RenderAndGetText(LogEvent logEvent, string outputTemplate, RichTextBoxSinkOptions options) + { + _richTextBox.Clear(); + var renderer = new TemplateRenderer(options.Theme, outputTemplate, options.FormatProvider, options); + renderer.Render(logEvent, _canvas); + return _richTextBox.Text.TrimEnd('\n', '\r'); + } + public virtual void Dispose() { GC.SuppressFinalize(this); diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs index 1b36c38..70a0119 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs @@ -45,6 +45,9 @@ public static class RichTextBoxSinkLoggerConfigurationExtensions /// Format provider, or null for invariant culture. /// Minimum log level for events to be written. /// Optional switch to change the minimum log level at runtime. + /// If true, formats JSON values with indentation and line breaks. Defaults to false. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. + /// If true (default), uses spaces for indentation; otherwise uses tabs. /// The logger configuration, for chaining. public static LoggerConfiguration RichTextBox( this LoggerSinkConfiguration sinkConfiguration, @@ -56,12 +59,15 @@ public static LoggerConfiguration RichTextBox( string outputTemplate = OutputTemplate, IFormatProvider? formatProvider = null, LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose, - LoggingLevelSwitch? levelSwitch = null) + LoggingLevelSwitch? levelSwitch = null, + bool prettyPrintJson = false, + int indentSize = 4, + bool useSpacesForIndent = true) { var appliedTheme = theme ?? ThemePresets.Literate; var appliedFormatProvider = formatProvider ?? CultureInfo.InvariantCulture; - var renderer = new TemplateRenderer(appliedTheme, outputTemplate, appliedFormatProvider); - var options = new RichTextBoxSinkOptions(appliedTheme, autoScroll, maxLogLines, outputTemplate, appliedFormatProvider); + var options = new RichTextBoxSinkOptions(appliedTheme, autoScroll, maxLogLines, outputTemplate, appliedFormatProvider, prettyPrintJson, indentSize, useSpacesForIndent); + var renderer = new TemplateRenderer(appliedTheme, outputTemplate, appliedFormatProvider, options); richTextBoxSink = new RichTextBoxSink(richTextBoxControl, options, renderer); return sinkConfiguration.Sink(richTextBoxSink, minimumLogEventLevel, levelSwitch); } @@ -78,6 +84,9 @@ public static LoggerConfiguration RichTextBox( /// Format provider, or null for invariant culture. /// Minimum log level for events to be written. /// Optional switch to change the minimum log level at runtime. + /// If true, formats JSON values with indentation and line breaks. Defaults to false. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. + /// If true (default), uses spaces for indentation; otherwise uses tabs. /// The logger configuration, for chaining. public static LoggerConfiguration RichTextBox( this LoggerSinkConfiguration sinkConfiguration, @@ -88,7 +97,10 @@ public static LoggerConfiguration RichTextBox( string outputTemplate = OutputTemplate, IFormatProvider? formatProvider = null, LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose, - LoggingLevelSwitch? levelSwitch = null) + LoggingLevelSwitch? levelSwitch = null, + bool prettyPrintJson = false, + int indentSize = 4, + bool useSpacesForIndent = true) { return RichTextBox( sinkConfiguration, @@ -100,7 +112,10 @@ public static LoggerConfiguration RichTextBox( outputTemplate, formatProvider, minimumLogEventLevel, - levelSwitch); + levelSwitch, + prettyPrintJson, + indentSize, + useSpacesForIndent); } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs index 50eecf6..59c24d3 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs @@ -29,13 +29,19 @@ namespace Serilog.Sinks.RichTextBoxForms.Formatting public class DisplayValueFormatter : ValueFormatter { private readonly IFormatProvider? _formatProvider; + private readonly bool _prettyPrintJson; + private readonly int _indentSize; + private readonly bool _useSpacesForIndent; private readonly StringBuilder _scalarBuilder = new(); private readonly StringBuilder _literalBuilder = new(64); private JsonValueFormatter? _jsonValueFormatter; - public DisplayValueFormatter(Theme theme, IFormatProvider? formatProvider) : base(theme, formatProvider) + public DisplayValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrintJson = false, int indentSize = 4, bool useSpacesForIndent = true) : base(theme, formatProvider) { _formatProvider = formatProvider; + _prettyPrintJson = prettyPrintJson; + _indentSize = indentSize; + _useSpacesForIndent = useSpacesForIndent; } private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas, string? format, bool isLiteral) @@ -100,7 +106,7 @@ protected override bool VisitDictionaryValue(ValueFormatterState state, Dictiona { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider); + _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _indentSize, _useSpacesForIndent); _jsonValueFormatter.Format(dictionary, state.Canvas, state.Format, state.IsLiteral); return true; } @@ -136,7 +142,7 @@ protected override bool VisitSequenceValue(ValueFormatterState state, SequenceVa { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider); + _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _indentSize, _useSpacesForIndent); _jsonValueFormatter.Format(sequence, state.Canvas, state.Format, state.IsLiteral); return true; } @@ -163,7 +169,7 @@ protected override bool VisitStructureValue(ValueFormatterState state, Structure { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider); + _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _indentSize, _useSpacesForIndent); _jsonValueFormatter.Format(structure, state.Canvas, state.Format, state.IsLiteral); return true; } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs index 11c79f3..e93f01f 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs @@ -30,13 +30,23 @@ namespace Serilog.Sinks.RichTextBoxForms.Formatting public class JsonValueFormatter : ValueFormatter { private readonly IFormatProvider? _formatProvider; + private readonly bool _prettyPrint; + private readonly int _indentSize; + private readonly bool _useSpacesForIndent; private readonly StringBuilder _literalBuilder = new(64); private readonly StringBuilder _scalarBuilder = new(); - private readonly StringBuilder _jsonStringBuilder = new(); - public JsonValueFormatter(Theme theme, IFormatProvider? formatProvider) : base(theme, formatProvider) + public JsonValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrint = false, int indentSize = 4, bool useSpacesForIndent = true) : base(theme, formatProvider) { _formatProvider = formatProvider; + _prettyPrint = prettyPrint; + _indentSize = indentSize; + _useSpacesForIndent = useSpacesForIndent; + } + + protected override ValueFormatterState CreateInitialState(IRtfCanvas canvas, string format, bool isLiteral) + { + return new ValueFormatterState(canvas, format, isLiteral, 0, _useSpacesForIndent, _indentSize, true); } protected override bool VisitScalarValue(ValueFormatterState state, ScalarValue scalar) @@ -47,81 +57,212 @@ protected override bool VisitScalarValue(ValueFormatterState state, ScalarValue protected override bool VisitSequenceValue(ValueFormatterState state, SequenceValue sequence) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); + if (_prettyPrint) + { + var indentState = state.ToIndentUp(); + if (sequence.Elements.Count > 0) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "[\n" + indentState.GetIndentation()); + } + else + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); + } - var delimiter = string.Empty; - foreach (var propertyValue in sequence.Elements) + var delimiter = string.Empty; + foreach (var propertyValue in sequence.Elements) + { + if (!string.IsNullOrEmpty(delimiter)) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + } + + delimiter = ",\n" + indentState.GetIndentation(); + Visit(indentState, propertyValue); + } + + if (sequence.Elements.Count > 0) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "\n" + state.GetIndentation() + "]"); + } + else + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "]"); + } + } + else { - if (!string.IsNullOrEmpty(delimiter)) + Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); + + var delimiter = string.Empty; + foreach (var propertyValue in sequence.Elements) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + if (!string.IsNullOrEmpty(delimiter)) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + } + + delimiter = ", "; + Visit(state, propertyValue); } - delimiter = ", "; - Visit(state, propertyValue); + Theme.Render(state.Canvas, StyleToken.TertiaryText, "]"); } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "]"); return true; } protected override bool VisitStructureValue(ValueFormatterState state, StructureValue structure) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); - - var delimiter = string.Empty; - foreach (var eventProperty in structure.Properties) + if (_prettyPrint) { - if (!string.IsNullOrEmpty(delimiter)) + var indentState = state.ToIndentUp(); + if (structure.Properties.Count > 0 || structure.TypeTag != null) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "{\n" + indentState.GetIndentation()); + } + else + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); + } + + var delimiter = string.Empty; + foreach (var eventProperty in structure.Properties) + { + if (!string.IsNullOrEmpty(delimiter)) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + } + + delimiter = ",\n" + indentState.GetIndentation(); + Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString(eventProperty.Name)); + Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + Visit(indentState.Next(), eventProperty.Value); + } + + if (structure.TypeTag != null) { Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString("$type")); + Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + Theme.Render(state.Canvas, StyleToken.String, GetQuotedJsonString(structure.TypeTag)); } - delimiter = ", "; - Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString(eventProperty.Name)); - Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); - Visit(state.Next(), eventProperty.Value); + if (structure.Properties.Count > 0 || structure.TypeTag != null) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "\n" + state.GetIndentation() + "}"); + } + else + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); + } } - - if (structure.TypeTag != null) + else { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); - Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString("$type")); - Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); - Theme.Render(state.Canvas, StyleToken.String, GetQuotedJsonString(structure.TypeTag)); + Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); + + var delimiter = string.Empty; + foreach (var eventProperty in structure.Properties) + { + if (!string.IsNullOrEmpty(delimiter)) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + } + + delimiter = ", "; + Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString(eventProperty.Name)); + Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + Visit(state.Next(), eventProperty.Value); + } + + if (structure.TypeTag != null) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString("$type")); + Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + Theme.Render(state.Canvas, StyleToken.String, GetQuotedJsonString(structure.TypeTag)); + } + + Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); return true; } protected override bool VisitDictionaryValue(ValueFormatterState state, DictionaryValue dictionary) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); - - var delimiter = string.Empty; - foreach (var (scalar, propertyValue) in dictionary.Elements) + if (_prettyPrint) { - if (!string.IsNullOrEmpty(delimiter)) + var indentState = state.ToIndentUp(); + if (dictionary.Elements.Count > 0) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Theme.Render(state.Canvas, StyleToken.TertiaryText, "{\n" + indentState.GetIndentation()); + } + else + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); + } + + var delimiter = string.Empty; + foreach (var (scalar, propertyValue) in dictionary.Elements) + { + if (!string.IsNullOrEmpty(delimiter)) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + } + + delimiter = ",\n" + indentState.GetIndentation(); + var style = scalar.Value switch + { + null => StyleToken.Null, + string => StyleToken.String, + _ => StyleToken.Scalar + }; + + Theme.Render(state.Canvas, style, GetQuotedJsonString(scalar.Value?.ToString() ?? "null")); + Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + + Visit(indentState.Next(), propertyValue); } - delimiter = ", "; - var style = scalar.Value switch + if (dictionary.Elements.Count > 0) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "\n" + state.GetIndentation() + "}"); + } + else { - null => StyleToken.Null, - string => StyleToken.String, - _ => StyleToken.Scalar - }; + Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); + } + } + else + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); + + var delimiter = string.Empty; + foreach (var (scalar, propertyValue) in dictionary.Elements) + { + if (!string.IsNullOrEmpty(delimiter)) + { + Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + } - Theme.Render(state.Canvas, style, GetQuotedJsonString(scalar.Value?.ToString() ?? "null")); - Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + delimiter = ", "; + var style = scalar.Value switch + { + null => StyleToken.Null, + string => StyleToken.String, + _ => StyleToken.Scalar + }; + + Theme.Render(state.Canvas, style, GetQuotedJsonString(scalar.Value?.ToString() ?? "null")); + Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + + Visit(state.Next(), propertyValue); + } - Visit(state.Next(), propertyValue); + Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); return true; } @@ -200,6 +341,12 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas) return; default: + if (value is ValueType and (int or uint or long or ulong or decimal or byte or sbyte or short or ushort)) + { + Theme.Render(canvas, StyleToken.Number, ((IFormattable)value).ToString(null, CultureInfo.InvariantCulture)); + return; + } + if (value is IFormattable formattable) { RenderFormattable(canvas, formattable, null); @@ -213,61 +360,87 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas) scalar.Render(writer, null, _formatProvider); } - Theme.Render(canvas, StyleToken.Scalar, _scalarBuilder.ToString()); + Theme.Render(canvas, StyleToken.Scalar, GetQuotedJsonString(_scalarBuilder.ToString())); return; } } - private static void WriteQuotedJsonString(StringBuilder builder, string str) + /// + /// Write a valid JSON string literal, escaping as necessary. + /// Optimized version that avoids unnecessary string operations. + /// + /// The string value to write. + public static string GetQuotedJsonString(string str) { - builder.Append('\"'); - - foreach (var c in str) + using (var output = new StringWriter()) { - switch (c) + output.Write('\"'); + + var cleanSegmentStart = 0; + var anyEscaped = false; + + for (var i = 0; i < str.Length; ++i) { - case '"': - builder.Append("\\\""); - break; - - case '\\': - builder.Append(@"\\"); - break; - - case '\n': - builder.Append("\\n"); - break; - - case '\r': - builder.Append("\\r"); - break; - - case '\f': - builder.Append("\\f"); - break; - - case '\t': - builder.Append("\\t"); - break; - - case < (char)32: - builder.Append("\\u"); - builder.Append(((int)c).ToString("X4")); - break; - - default: - builder.Append(c); - break; + var c = str[i]; + if (c < (char)32 || c == '\\' || c == '"') + { + anyEscaped = true; + + if (i > cleanSegmentStart) + { + output.Write(str.Substring(cleanSegmentStart, i - cleanSegmentStart)); + } + cleanSegmentStart = i + 1; + + switch (c) + { + case '"': + output.Write("\\\""); + break; + + case '\\': + output.Write("\\\\"); + break; + + case '\n': + output.Write("\\n"); + break; + + case '\r': + output.Write("\\r"); + break; + + case '\f': + output.Write("\\f"); + break; + + case '\t': + output.Write("\\t"); + break; + + default: + output.Write("\\u"); + output.Write(((int)c).ToString("X4")); + break; + } + } } - } - builder.Append('\"'); - } - private string GetQuotedJsonString(string str) - { - _jsonStringBuilder.Clear(); - WriteQuotedJsonString(_jsonStringBuilder, str); - return _jsonStringBuilder.ToString(); + if (anyEscaped) + { + if (cleanSegmentStart < str.Length) + { + output.Write(str.Substring(cleanSegmentStart)); + } + } + else + { + output.Write(str); + } + + output.Write('\"'); + return output.ToString(); + } } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatter.cs index 9fd08e4..6f76019 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatter.cs @@ -40,7 +40,12 @@ protected ValueFormatter(Theme theme, IFormatProvider? formatProvider = null) public void Format(LogEventPropertyValue value, IRtfCanvas canvas, string format, bool isLiteral) { - Visit(new ValueFormatterState(canvas, format, isLiteral), value); + Visit(CreateInitialState(canvas, format, isLiteral), value); + } + + protected virtual ValueFormatterState CreateInitialState(IRtfCanvas canvas, string format, bool isLiteral) + { + return new ValueFormatterState(canvas, format, isLiteral); } /// diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs index 3729a91..ced0e93 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs @@ -22,20 +22,51 @@ namespace Serilog.Sinks.RichTextBoxForms.Formatting { public readonly struct ValueFormatterState { - public ValueFormatterState(IRtfCanvas canvas, string format, bool isLiteral) + public ValueFormatterState(IRtfCanvas canvas, string format, bool isLiteral, int indentLevel = 0, bool useSpacesForIndent = true, int indentSize = 4, bool isTopLevel = true) { Canvas = canvas; Format = format; IsLiteral = isLiteral; + IndentLevel = indentLevel; + UseSpacesForIndent = useSpacesForIndent; + IndentSize = indentSize; + IsTopLevel = isTopLevel; } public string Format { get; } public bool IsLiteral { get; } public IRtfCanvas Canvas { get; } + public int IndentLevel { get; } + public bool UseSpacesForIndent { get; } + public int IndentSize { get; } + public bool IsTopLevel { get; } public ValueFormatterState Next(string? format = null) { - return new ValueFormatterState(Canvas, format ?? Format, IsLiteral); + return new ValueFormatterState(Canvas, format ?? Format, IsLiteral, IndentLevel, UseSpacesForIndent, IndentSize, false); + } + + public ValueFormatterState ToIndentUp() + { + return new ValueFormatterState(Canvas, Format, IsLiteral, IndentLevel + 1, UseSpacesForIndent, IndentSize, false); + } + + public ValueFormatterState ToIndentDown() + { + return new ValueFormatterState(Canvas, Format, IsLiteral, IndentLevel > 0 ? IndentLevel - 1 : 0, UseSpacesForIndent, IndentSize, false); + } + + public string GetIndentation() + { + if (IndentLevel <= 0) + { + return string.Empty; + } + + var totalSpaces = IndentLevel * IndentSize; + return UseSpacesForIndent + ? new string(' ', totalSpaces) + : new string('\t', IndentLevel); } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs index bd34079..1e09a49 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs @@ -18,6 +18,7 @@ using Serilog.Events; using Serilog.Parsing; +using Serilog.Sinks.RichTextBoxForms; using Serilog.Sinks.RichTextBoxForms.Formatting; using Serilog.Sinks.RichTextBoxForms.Rtf; using Serilog.Sinks.RichTextBoxForms.Themes; @@ -29,14 +30,14 @@ public class MessageTemplateTokenRenderer : ITokenRenderer { private readonly MessageTemplateRenderer _renderer; - public MessageTemplateTokenRenderer(Theme theme, PropertyToken token, IFormatProvider? formatProvider) + public MessageTemplateTokenRenderer(Theme theme, PropertyToken token, IFormatProvider? formatProvider, RichTextBoxSinkOptions? options = null) { var isLiteral = token.Format?.Contains("l") == true; var isJson = token.Format?.Contains("j") == true; ValueFormatter valueFormatter = isJson - ? new JsonValueFormatter(theme, formatProvider) - : new DisplayValueFormatter(theme, formatProvider); + ? new JsonValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.IndentSize ?? 4, options?.UseSpacesForIndent ?? true) + : new DisplayValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.IndentSize ?? 4, options?.UseSpacesForIndent ?? true); _renderer = new MessageTemplateRenderer(theme, valueFormatter, isLiteral); } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs index 5033010..684d3f3 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs @@ -18,6 +18,7 @@ using Serilog.Events; using Serilog.Parsing; +using Serilog.Sinks.RichTextBoxForms; using Serilog.Sinks.RichTextBoxForms.Formatting; using Serilog.Sinks.RichTextBoxForms.Rtf; using Serilog.Sinks.RichTextBoxForms.Themes; @@ -32,11 +33,11 @@ public class PropertiesTokenRenderer : ITokenRenderer private readonly ValueFormatter _valueFormatter; private readonly HashSet _outputTemplateProperties; - public PropertiesTokenRenderer(Theme theme, PropertyToken token, MessageTemplate outputTemplate, IFormatProvider? formatProvider) + public PropertiesTokenRenderer(Theme theme, PropertyToken token, MessageTemplate outputTemplate, IFormatProvider? formatProvider, RichTextBoxSinkOptions? options = null) { _valueFormatter = token.Format?.Contains("j") == true - ? new JsonValueFormatter(theme, formatProvider) - : new DisplayValueFormatter(theme, formatProvider); + ? new JsonValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.IndentSize ?? 4, options?.UseSpacesForIndent ?? true) + : new DisplayValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.IndentSize ?? 4, options?.UseSpacesForIndent ?? true); _outputTemplateProperties = new HashSet( outputTemplate.Tokens.OfType().Select(p => p.PropertyName)); diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs index be30ec4..90305d9 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs @@ -19,6 +19,7 @@ using Serilog.Events; using Serilog.Formatting.Display; using Serilog.Parsing; +using Serilog.Sinks.RichTextBoxForms; using Serilog.Sinks.RichTextBoxForms.Rtf; using Serilog.Sinks.RichTextBoxForms.Themes; using System; @@ -31,7 +32,7 @@ public class TemplateRenderer : ITokenRenderer private const string OutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"; private readonly List _renderers; - public TemplateRenderer(Theme theme, string outputTemplate = OutputTemplate, IFormatProvider? formatProvider = null) + public TemplateRenderer(Theme theme, string outputTemplate = OutputTemplate, IFormatProvider? formatProvider = null, RichTextBoxSinkOptions? options = null) { if (string.IsNullOrEmpty(outputTemplate)) { @@ -72,7 +73,7 @@ public TemplateRenderer(Theme theme, string outputTemplate = OutputTemplate, IFo case OutputProperties.MessagePropertyName: { - _renderers.Add(new MessageTemplateTokenRenderer(theme, propertyToken, formatProvider)); + _renderers.Add(new MessageTemplateTokenRenderer(theme, propertyToken, formatProvider, options)); break; } @@ -84,7 +85,7 @@ public TemplateRenderer(Theme theme, string outputTemplate = OutputTemplate, IFo case OutputProperties.PropertiesPropertyName: { - _renderers.Add(new PropertiesTokenRenderer(theme, propertyToken, template, formatProvider)); + _renderers.Add(new PropertiesTokenRenderer(theme, propertyToken, template, formatProvider, options)); break; } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs index 365eac4..b9437ff 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs @@ -45,7 +45,7 @@ public RichTextBoxSink(RichTextBox richTextBox, RichTextBoxSinkOptions options, { _options = options; _richTextBox = richTextBox; - _renderer = renderer ?? new TemplateRenderer(options.Theme, options.OutputTemplate, options.FormatProvider); + _renderer = renderer ?? new TemplateRenderer(options.Theme, options.OutputTemplate, options.FormatProvider, options); _tokenSource = new CancellationTokenSource(); _buffer = new ConcurrentCircularBuffer(options.MaxLogLines); diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs index 53551f7..26b7e07 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs @@ -36,18 +36,27 @@ public class RichTextBoxSinkOptions /// Maximum number of log events retained in the in-memory circular buffer and rendered in the control. /// Serilog output template that controls textual formatting of each log event. /// Optional culture-specific or custom formatting provider used when rendering scalar values; null for the invariant culture. + /// When true, formats JSON values with indentation and line breaks for better readability. Defaults to false. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. + /// When true (default), uses spaces for indentation; otherwise uses tabs. public RichTextBoxSinkOptions( Theme theme, bool autoScroll = true, int maxLogLines = 256, string outputTemplate = DefaultOutputTemplate, - IFormatProvider? formatProvider = null) + IFormatProvider? formatProvider = null, + bool prettyPrintJson = false, + int indentSize = 4, + bool useSpacesForIndent = true) { AutoScroll = autoScroll; Theme = theme; MaxLogLines = maxLogLines; OutputTemplate = outputTemplate; FormatProvider = formatProvider ?? CultureInfo.InvariantCulture; + PrettyPrintJson = prettyPrintJson; + IndentSize = indentSize; + UseSpacesForIndent = useSpacesForIndent; } public bool AutoScroll { get; set; } @@ -68,5 +77,21 @@ public int MaxLogLines public string OutputTemplate { get; } public IFormatProvider? FormatProvider { get; } + + /// + /// When true, formats JSON values (when using the :j format specifier) with indentation and line breaks for better readability. + /// Defaults to false for compact JSON output. + /// + public bool PrettyPrintJson { get; } + + /// + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. + /// + public int IndentSize { get; } + + /// + /// When true (default), uses spaces for indentation; otherwise uses tabs. + /// + public bool UseSpacesForIndent { get; } } } \ No newline at end of file From aca1ad753b3a5bbbafd11082f9d6f5ee649e6418 Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 14:48:41 +0100 Subject: [PATCH 02/15] Add new tests Updates the package version to 3.2.0. Adds integration tests for empty collections, dictionaries with null and non-string keys, and scalar/non-formattable objects. --- .../Integration/JsonFormattingTests.cs | 74 ++++++++++++++++++- ....Sinks.RichTextBox.WinForms.Colored.csproj | 7 +- .../Rendering/MessageTemplateTokenRenderer.cs | 1 - .../Rendering/PropertiesTokenRenderer.cs | 1 - .../Rendering/TemplateRenderer.cs | 1 - 5 files changed, 74 insertions(+), 10 deletions(-) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs index 3baacc5..59b8639 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs @@ -190,8 +190,9 @@ public void PrettyPrintJson_FormatsNestedStructures() public void PrettyPrintJson_EmptyCollectionsFormatCorrectly() { var emptyArray = new LogEventProperty("EmptyArray", new SequenceValue(Array.Empty())); + var emptyDict = new LogEventProperty("EmptyDict", new DictionaryValue(new Dictionary())); var emptyObject = new LogEventProperty("EmptyObject", new StructureValue(Array.Empty(), null)); - var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("Array: {EmptyArray:j}, Object: {EmptyObject:j}"), new[] { emptyArray, emptyObject }); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("Array: {EmptyArray:j}, Dict: {EmptyDict:j}, Object: {EmptyObject:j}"), new[] { emptyArray, emptyDict, emptyObject }); var options = new RichTextBoxSinkOptions( theme: _defaultTheme, @@ -201,6 +202,7 @@ public void PrettyPrintJson_EmptyCollectionsFormatCorrectly() var result = RenderAndGetText(logEvent, "{Message:l}", options); Assert.Contains("Array: []", result); + Assert.Contains("Dict: {}", result); Assert.Contains("Object: {}", result); } @@ -249,9 +251,75 @@ public void CompactJson_StillWorksByDefault() { var complexProp = new LogEventProperty("ComplexProp", new StructureValue(new[] { new LogEventProperty("Id", new ScalarValue(123)) }, "MyObj")); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("Value: {ComplexProp}"), new[] { complexProp }); - - // Default behavior (compact) Assert.Equal("Value: {\"Id\": 123, \"$type\": \"MyObj\"}", RenderAndGetText(logEvent, "{Message:j}")); } + + [Fact] + public void ScalarValue_IFormattableButNotNumericValueType() + { + var enumValue = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance; + var prop = new LogEventProperty("EnumProp", new ScalarValue(enumValue)); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{EnumProp:j}"), new[] { prop }); + + var result = RenderAndGetText(logEvent, "{Message:j}"); + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + [Fact] + public void ScalarValue_NonFormattableObject() + { + var plainObject = new object(); + var prop = new LogEventProperty("ObjectProp", new ScalarValue(plainObject)); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{ObjectProp:j}"), new[] { prop }); + + var result = RenderAndGetText(logEvent, "{Message:j}"); + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.StartsWith("\"", result); + Assert.EndsWith("\"", result); + } + + [Fact] + public void PrettyPrintJson_DictionaryWithNullKey() + { + var dict = new Dictionary + { + { new ScalarValue(null), new ScalarValue("value") } + }; + var dictProp = new LogEventProperty("DictProp", new DictionaryValue(dict)); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{DictProp:j}"), new[] { dictProp }); + + var options = new RichTextBoxSinkOptions( + theme: _defaultTheme, + prettyPrintJson: true, + indentSize: 4, + useSpacesForIndent: true); + + var result = RenderAndGetText(logEvent, "{Message:l}", options); + Assert.Contains("\"null\"", result); + Assert.Contains("\"value\"", result); + } + + [Fact] + public void PrettyPrintJson_DictionaryWithNonStringKey() + { + var dict = new Dictionary + { + { new ScalarValue(123), new ScalarValue("value") } + }; + var dictProp = new LogEventProperty("DictProp", new DictionaryValue(dict)); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{DictProp:j}"), new[] { dictProp }); + + var options = new RichTextBoxSinkOptions( + theme: _defaultTheme, + prettyPrintJson: true, + indentSize: 4, + useSpacesForIndent: true); + + var result = RenderAndGetText(logEvent, "{Message:l}", options); + Assert.Contains("\"123\"", result); + Assert.Contains("\"value\"", result); + } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj b/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj index 12c8b51..14c2c0c 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj @@ -22,14 +22,13 @@ net462;net471;net6.0-windows;net8.0-windows;net9.0-windows;netcoreapp3.0-windows;netcoreapp3.1-windows true true - 3.1.3 + 3.2.0 -- Fixed performance issues when logging large numbers of complex messages that could cause application freezing. -- Fixed `ArgumentOutOfRangeException` thrown from `RtfBuilder.Clear()` +- Added support for JSON pretty-printing in log messages. See repository for more information: https://github.com/vonhoff/Serilog.Sinks.RichTextBox.WinForms.Colored - + 7.0 windows en-US diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs index 1e09a49..567c4c0 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs @@ -18,7 +18,6 @@ using Serilog.Events; using Serilog.Parsing; -using Serilog.Sinks.RichTextBoxForms; using Serilog.Sinks.RichTextBoxForms.Formatting; using Serilog.Sinks.RichTextBoxForms.Rtf; using Serilog.Sinks.RichTextBoxForms.Themes; diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs index 684d3f3..845f862 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs @@ -18,7 +18,6 @@ using Serilog.Events; using Serilog.Parsing; -using Serilog.Sinks.RichTextBoxForms; using Serilog.Sinks.RichTextBoxForms.Formatting; using Serilog.Sinks.RichTextBoxForms.Rtf; using Serilog.Sinks.RichTextBoxForms.Themes; diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs index 90305d9..b60a576 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs @@ -19,7 +19,6 @@ using Serilog.Events; using Serilog.Formatting.Display; using Serilog.Parsing; -using Serilog.Sinks.RichTextBoxForms; using Serilog.Sinks.RichTextBoxForms.Rtf; using Serilog.Sinks.RichTextBoxForms.Themes; using System; From 2f2a980399d3911dfce5aea3a7e2269caee42223 Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 15:00:20 +0100 Subject: [PATCH 03/15] Create TokenRendererTests.cs --- .../Integration/TokenRendererTests.cs | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs new file mode 100644 index 0000000..30f58c7 --- /dev/null +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs @@ -0,0 +1,259 @@ +using Serilog.Events; +using Serilog.Parsing; +using Serilog.Sinks.RichTextBoxForms.Rendering; +using Serilog.Sinks.RichTextBoxForms.Themes; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Serilog.Tests.Integration +{ + public class TokenRendererTests : RichTextBoxSinkTestBase + { + [Fact] + public void ExceptionTokenRenderer_RendersExceptionWithStackFrames() + { + Exception? exception = null; + try + { + throw new InvalidOperationException("Test exception"); + } + catch (Exception ex) + { + exception = ex; + } + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Error, + exception, + _parser.Parse("Error occurred"), + Array.Empty()); + + var renderer = new ExceptionTokenRenderer(_defaultTheme); + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("InvalidOperationException", result); + Assert.Contains("Test exception", result); + + var lines = result.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + Assert.True(lines.Length > 1, $"Exception should have multiple lines including stack trace. Got: {lines.Length} lines. Text: {result}"); + } + + [Fact] + public void ExceptionTokenRenderer_RendersMultipleLines() + { + Exception? exception = null; + try + { + throw new InvalidOperationException("Test exception"); + } + catch (Exception ex) + { + exception = ex; + } + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Error, + exception, + _parser.Parse("Error occurred"), + Array.Empty()); + + var renderer = new ExceptionTokenRenderer(_defaultTheme); + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + var lines = result.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + Assert.True(lines.Length > 1, $"Exception should have multiple lines. Got: {lines.Length} lines. Text: {result}"); + } + + [Fact] + public void ExceptionTokenRenderer_HandlesNullException() + { + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + _parser.Parse("No exception"), + Array.Empty()); + + var renderer = new ExceptionTokenRenderer(_defaultTheme); + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Empty(result); + } + + [Fact] + public void EventPropertyTokenRenderer_RendersNonStringScalarValue() + { + var template = _parser.Parse("Number: {Number}"); + var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "Number"); + var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + template, + new[] { new LogEventProperty("Number", new ScalarValue(42)) }); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("42", result); + } + + [Fact] + public void EventPropertyTokenRenderer_RendersBooleanValue() + { + var template = _parser.Parse("IsValid: {IsValid}"); + var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "IsValid"); + var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + template, + new[] { new LogEventProperty("IsValid", new ScalarValue(true)) }); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("True", result); + } + + [Fact] + public void EventPropertyTokenRenderer_RendersStructureValue() + { + var template = _parser.Parse("User: {User}"); + var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "User"); + var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + + var structureValue = new StructureValue(new[] + { + new LogEventProperty("Id", new ScalarValue(123)), + new LogEventProperty("Name", new ScalarValue("John")) + }, "User"); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + template, + new[] { new LogEventProperty("User", structureValue) }); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("Id", result); + Assert.Contains("123", result); + Assert.Contains("Name", result); + Assert.Contains("John", result); + } + + [Fact] + public void EventPropertyTokenRenderer_RendersSequenceValue() + { + var template = _parser.Parse("Items: {Items}"); + var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "Items"); + var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + + var sequenceValue = new SequenceValue(new[] + { + new ScalarValue(1), + new ScalarValue(2), + new ScalarValue(3) + }); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + template, + new[] { new LogEventProperty("Items", sequenceValue) }); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("1", result); + Assert.Contains("2", result); + Assert.Contains("3", result); + } + + [Fact] + public void EventPropertyTokenRenderer_RendersDictionaryValue() + { + var template = _parser.Parse("Config: {Config}"); + var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "Config"); + var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + + var dict = new Dictionary + { + { new ScalarValue("key1"), new ScalarValue("value1") }, + { new ScalarValue("key2"), new ScalarValue(42) } + }; + var dictionaryValue = new DictionaryValue(dict); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + template, + new[] { new LogEventProperty("Config", dictionaryValue) }); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("key1", result); + Assert.Contains("value1", result); + Assert.Contains("key2", result); + Assert.Contains("42", result); + } + + [Fact] + public void EventPropertyTokenRenderer_HandlesMissingProperty() + { + var template = _parser.Parse("Missing: {Missing}"); + var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "Missing"); + var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + template, + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Empty(result); + } + + [Fact] + public void EventPropertyTokenRenderer_RendersStringValue() + { + var template = _parser.Parse("Message: {Message}"); + var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "Message"); + var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + template, + new[] { new LogEventProperty("Message", new ScalarValue("Hello World")) }); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("Hello World", result); + } + } +} + From 1a8c356187b761b74d74654b07c4dc6af7594b98 Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 15:13:07 +0100 Subject: [PATCH 04/15] Improve LevelTokenRenderer handling and add tests Enhanced LevelTokenRenderer to gracefully handle invalid log level indices and expanded test coverage for various level formatting scenarios. Also cleaned up unused usings and removed redundant tests in TokenRendererTests. --- .github/FUNDING.yml | 1 + .github/PULL_REQUEST_TEMPLATE.md | 2 +- .../Integration/JsonFormattingTests.cs | 6 +- .../Integration/TokenRendererTests.cs | 276 ++++++++++++++---- .../Rendering/LevelTokenRenderer.cs | 13 +- 5 files changed, 235 insertions(+), 63 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4a7c7eb..4fc762e 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ github: vonhoff +ko-fi: vonhoff \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 02586a1..0528c8c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ -**Motivation and Context** +**Context** diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs index 59b8639..360ed82 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs @@ -1,7 +1,5 @@ -using System; using Serilog.Events; using Serilog.Sinks.RichTextBoxForms; -using Serilog.Sinks.RichTextBoxForms.Themes; using Xunit; namespace Serilog.Tests.Integration @@ -260,7 +258,7 @@ public void ScalarValue_IFormattableButNotNumericValueType() var enumValue = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance; var prop = new LogEventProperty("EnumProp", new ScalarValue(enumValue)); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{EnumProp:j}"), new[] { prop }); - + var result = RenderAndGetText(logEvent, "{Message:j}"); Assert.NotNull(result); Assert.NotEmpty(result); @@ -272,7 +270,7 @@ public void ScalarValue_NonFormattableObject() var plainObject = new object(); var prop = new LogEventProperty("ObjectProp", new ScalarValue(plainObject)); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{ObjectProp:j}"), new[] { prop }); - + var result = RenderAndGetText(logEvent, "{Message:j}"); Assert.NotNull(result); Assert.NotEmpty(result); diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs index 30f58c7..953713d 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs @@ -1,10 +1,6 @@ using Serilog.Events; using Serilog.Parsing; using Serilog.Sinks.RichTextBoxForms.Rendering; -using Serilog.Sinks.RichTextBoxForms.Themes; -using System; -using System.Collections.Generic; -using System.Linq; using Xunit; namespace Serilog.Tests.Integration @@ -37,37 +33,9 @@ public void ExceptionTokenRenderer_RendersExceptionWithStackFrames() var result = _richTextBox.Text; Assert.Contains("InvalidOperationException", result); Assert.Contains("Test exception", result); - - var lines = result.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); - Assert.True(lines.Length > 1, $"Exception should have multiple lines including stack trace. Got: {lines.Length} lines. Text: {result}"); - } - - [Fact] - public void ExceptionTokenRenderer_RendersMultipleLines() - { - Exception? exception = null; - try - { - throw new InvalidOperationException("Test exception"); - } - catch (Exception ex) - { - exception = ex; - } - - var logEvent = new LogEvent( - DateTimeOffset.Now, - LogEventLevel.Error, - exception, - _parser.Parse("Error occurred"), - Array.Empty()); - - var renderer = new ExceptionTokenRenderer(_defaultTheme); - renderer.Render(logEvent, _canvas); - var result = _richTextBox.Text; var lines = result.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); - Assert.True(lines.Length > 1, $"Exception should have multiple lines. Got: {lines.Length} lines. Text: {result}"); + Assert.True(lines.Length > 1, $"Exception should have multiple lines including stack trace. Got: {lines.Length} lines. Text: {result}"); } [Fact] @@ -107,26 +75,6 @@ public void EventPropertyTokenRenderer_RendersNonStringScalarValue() Assert.Contains("42", result); } - [Fact] - public void EventPropertyTokenRenderer_RendersBooleanValue() - { - var template = _parser.Parse("IsValid: {IsValid}"); - var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "IsValid"); - var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); - - var logEvent = new LogEvent( - DateTimeOffset.Now, - LogEventLevel.Information, - null, - template, - new[] { new LogEventProperty("IsValid", new ScalarValue(true)) }); - - renderer.Render(logEvent, _canvas); - - var result = _richTextBox.Text; - Assert.Contains("True", result); - } - [Fact] public void EventPropertyTokenRenderer_RendersStructureValue() { @@ -254,6 +202,228 @@ public void EventPropertyTokenRenderer_RendersStringValue() var result = _richTextBox.Text; Assert.Contains("Hello World", result); } + + [Fact] + public void LevelTokenRenderer_WithLowercaseFormat_RendersCorrectly() + { + var template = _parser.Parse("Level: {Level:w3}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("inf", result); + } + + [Fact] + public void LevelTokenRenderer_WithTitleCaseFormat_RendersCorrectly() + { + var template = _parser.Parse("Level: {Level:t3}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Warning, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("Wrn", result); + } + + [Fact] + public void LevelTokenRenderer_WithUppercaseFormat_RendersCorrectly() + { + var template = _parser.Parse("Level: {Level:u3}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Error, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("ERR", result); + } + + [Fact] + public void LevelTokenRenderer_WithThreeDigitWidth_RendersCorrectly() + { + var template = _parser.Parse("Level: {Level:u10}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("INFORMATIO", result); + } + + [Fact] + public void LevelTokenRenderer_WithWidthLessThanOne_ReturnsEmpty() + { + var template = _parser.Parse("Level: {Level:u0}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Empty(result); + } + + [Fact] + public void LevelTokenRenderer_WithWidthGreaterThanFour_TruncatesAndFormats() + { + var template = _parser.Parse("Level: {Level:u5}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("INFOR", result); + } + + [Fact] + public void LevelTokenRenderer_WithInvalidFormatSpecifier_UsesTextFormatter() + { + var template = _parser.Parse("Level: {Level:x3}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("Information", result); + } + + [Fact] + public void LevelTokenRenderer_WithNonStandardFormatLength_UsesTextFormatter() + { + var template = _parser.Parse("Level: {Level:u}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Information, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("INFORMATION", result); + } + + [Fact] + public void LevelTokenRenderer_WithInvalidLevelIndex_HandlesGracefully() + { + var template = _parser.Parse("Level: {Level:u3}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var invalidLevel = (LogEventLevel)999; + var logEvent = new LogEvent( + DateTimeOffset.Now, + invalidLevel, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("999", result); + } + + [Fact] + public void LevelTokenRenderer_WithNegativeLevelIndex_HandlesGracefully() + { + var template = _parser.Parse("Level: {Level:u3}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var invalidLevel = (LogEventLevel)(-1); + var logEvent = new LogEvent( + DateTimeOffset.Now, + invalidLevel, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("-1", result); + } + + [Fact] + public void LevelTokenRenderer_WithWidthGreaterThanStringLength_DoesNotTruncate() + { + var template = _parser.Parse("Level: {Level:u20}"); + var levelToken = template.Tokens.OfType().Single(t => t.PropertyName == "Level"); + var renderer = new LevelTokenRenderer(_defaultTheme, levelToken); + + var logEvent = new LogEvent( + DateTimeOffset.Now, + LogEventLevel.Verbose, + null, + _parser.Parse("Test"), + Array.Empty()); + + renderer.Render(logEvent, _canvas); + + var result = _richTextBox.Text; + Assert.Contains("VERBOSE", result); + } } } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/LevelTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/LevelTokenRenderer.cs index a499f44..b535663 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/LevelTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/LevelTokenRenderer.cs @@ -82,6 +82,14 @@ public LevelTokenRenderer(Theme theme, PropertyToken levelToken) public void Render(LogEvent logEvent, IRtfCanvas canvas) { var levelIndex = (int)logEvent.Level; + if (levelIndex < 0 || levelIndex >= _monikers.Length) + { + var format = string.Empty; + var fallbackMoniker = TextFormatter.Format(logEvent.Level.ToString(), format); + _theme.Render(canvas, StyleToken.Text, fallbackMoniker); + return; + } + var moniker = _monikers[levelIndex]; var levelStyle = LevelStyles[levelIndex]; _theme.Render(canvas, levelStyle, moniker); @@ -119,11 +127,6 @@ private static string GetLevelMoniker(LogEventLevel value, string format = "") } var index = (int)value; - if (index is < 0 or > (int)LogEventLevel.Fatal) - { - return TextFormatter.Format(value.ToString(), format); - } - return format[0] switch { 'w' => LowercaseLevelMap[index][width - 1], From 9867081b2caf3e1b7be093d5298693712eb460bf Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 15:40:29 +0100 Subject: [PATCH 05/15] Update README.md Update funding config and improve README support section Corrected the Ko-fi key in FUNDING.yml. Refactored the README to clarify support options, improve FAQ answers, and reorganize contribution and support sections for better readability. --- .github/FUNDING.yml | 2 +- README.md | 33 +++++++++++++++++---------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4fc762e..7fe04ac 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ github: vonhoff -ko-fi: vonhoff \ No newline at end of file +ko_fi: vonhoff \ No newline at end of file diff --git a/README.md b/README.md index 1c70275..448dad5 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,9 @@ A [Serilog](https://github.com/serilog/serilog) sink that writes log events to a - Multiple theme presets with customization options - High-performance asynchronous processing - Line limit to control memory usage -- Pretty printing of JSON objects +- Support for pretty-printing of JSON objects - WCAG compliant color schemes based on the [Serilog WPF RichTextBox](https://github.com/serilog-contrib/serilog-sinks-richtextbox) sink. -> **Consider supporting the development of this project on Ko-fi!** -> -> Your support keeps this project alive. Even a small gesture means a lot. -> -> [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/vonhoff) - ## Getting Started Install the package from NuGet: @@ -74,23 +68,30 @@ The themes based on the original sinks are slightly adjusted to be [WCAG complia You can create your own custom themes by creating a new instance of the [Theme](Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Themes/Theme.cs) class and passing it to the `RichTextBox` extension method. Look at the [existing themes](Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Themes/ThemePresets.cs) for examples. -## Frequently Asked Questions +## FAQ ### Why is the package name so long? -Shorter alternatives were already reserved in the NuGet registry, so a more descriptive name was needed for this implementation. The name is a bit long, but it makes it easier to find the package in the NuGet registry. +Shorter alternatives were already reserved in NuGet. The descriptive name helps people find it more easily. + +### Why a WinForms RichTextBox and not WPF? + +This sink is designed for WinForms apps to avoid pulling in the WPF framework and its dependencies. + +## Support the Project 💖 + +This project has been maintained since 2022 and is still under active development. If you find it useful, please consider supporting it. Your support will help keep the project alive and allow me to dedicate more time to making improvements. You can support it through: -### Why use a WinForms RichTextBox instead of a WPF RichTextBox? +* [GitHub Sponsors](https://github.com/sponsors/vonhoff) +* [Ko-fi](https://ko-fi.com/vonhoff) -This sink is specifically designed for WinForms applications to avoid the WPF framework. Using a WPF-based logging component would require adding the entire WPF framework with all its dependencies, greatly increasing the size of the application. +Every contribution of any size helps sustain ongoing development. -## Support and Contribute +## Contributing -If you find value in this project, there are several ways you can contribute: +Contributions are welcome! Report issues, improve documentation, or submit pull requests. -- Give the [project](https://github.com/vonhoff/Serilog.Sinks.RichTextBox.WinForms.Colored) a star on GitHub ⭐ -- Support the project through [GitHub Sponsors](https://github.com/sponsors/vonhoff) -- Improve docs, report bugs, or submit PRs (see [CONTRIBUTING.md](CONTRIBUTING.md)) +See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. ## License From 205cde76bfbdab66eaf215784ffa2a5dfe92470a Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 15:54:32 +0100 Subject: [PATCH 06/15] Update build.yml --- .github/workflows/build.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6fabb83..1085236 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,15 +11,15 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET 8.0 - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v5 with: dotnet-version: 8.0.x - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.1 + uses: microsoft/setup-msbuild@v2 - name: Restore dependencies run: dotnet restore @@ -29,28 +29,25 @@ jobs: - name: Test with Coverage run: dotnet test --no-build --configuration Release --framework net8.0-windows --collect:"XPlat Code Coverage" --results-directory ./coverage - - name: Generate coverage report - uses: danielpalme/ReportGenerator-GitHub-Action@5.1.4 + uses: danielpalme/ReportGenerator-GitHub-Action@v5 with: reports: './coverage/**/coverage.cobertura.xml' targetdir: './coverage/report' reporttypes: 'Html;HtmlSummary' title: 'Code Coverage Report' - - name: Upload coverage report uses: actions/upload-artifact@v4 with: name: coverage-report path: './coverage/report' continue-on-error: true - - name: Check coverage threshold run: | $coverage = Select-Xml -Path "./coverage/**/coverage.cobertura.xml" -XPath "//coverage/@line-rate" | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value $coveragePercent = [math]::Round([double]$coverage * 100, 2) Write-Host "Current line coverage: $coveragePercent%" - if ($coveragePercent -lt 75) { - Write-Error "Code coverage ($coveragePercent%) is below the required threshold of 75%" + if ($coveragePercent -lt 80) { + Write-Error "Code coverage ($coveragePercent%) is below the required threshold of 80%" exit 1 } \ No newline at end of file From 9b0c2532ca66d5d3437a2bd0291bb7fc73ac0e8e Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 16:04:36 +0100 Subject: [PATCH 07/15] Removed redundant comments --- .../ConcurrentCircularBufferTests.cs | 13 ++----- .../Integration/BasicFormattingTests.cs | 34 ------------------- .../PropertiesTokenRendererTests.cs | 15 -------- .../Integration/SinkLifecycleTests.cs | 2 +- .../Integration/SpecialCaseFormattingTests.cs | 31 ++--------------- .../RichTextBoxSinkOptions.cs | 10 ------ 6 files changed, 5 insertions(+), 100 deletions(-) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Collections/ConcurrentCircularBufferTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Collections/ConcurrentCircularBufferTests.cs index 85da98c..10ece79 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Collections/ConcurrentCircularBufferTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Collections/ConcurrentCircularBufferTests.cs @@ -8,47 +8,40 @@ public class ConcurrentCircularBufferTests [Fact] public void TakeSnapshot_WithItemsLessThanCapacity_ReturnsAllItemsInOrder() { - // Arrange var buffer = new ConcurrentCircularBuffer(3); buffer.Add(1); buffer.Add(2); - // Act var snapshot = new List(); buffer.TakeSnapshot(snapshot); - // Assert Assert.Equal(new[] { 1, 2 }, snapshot); } [Fact] public void AddBeyondCapacity_OverwritesOldestItemsAndMaintainsOrder() { - // Arrange var buffer = new ConcurrentCircularBuffer(3); buffer.Add(1); buffer.Add(2); buffer.Add(3); buffer.Add(4); // Should overwrite the oldest item (1) - // Act var snapshot = new List(); buffer.TakeSnapshot(snapshot); - // Assert Assert.Equal(new[] { 2, 3, 4 }, snapshot); } [Fact] public void Clear_FollowedByAdds_SnapshotContainsOnlyNewItems() { - // Arrange var buffer = new ConcurrentCircularBuffer(3); buffer.Add(1); buffer.Add(2); buffer.Add(3); - // Act & Assert - After clear, snapshot should be empty + // After clear, snapshot should be empty buffer.Clear(); var snapshotAfterClear = new List(); buffer.TakeSnapshot(snapshotAfterClear); @@ -71,7 +64,6 @@ public void Clear_FollowedByAdds_SnapshotContainsOnlyNewItems() [Fact] public void Restore_AfterClear_ReturnsAllItemsAgain() { - // Arrange var buffer = new ConcurrentCircularBuffer(3); buffer.Add(1); buffer.Add(2); @@ -82,12 +74,11 @@ public void Restore_AfterClear_ReturnsAllItemsAgain() buffer.Add(4); buffer.Add(5); - // Act - Restore should make all items visible again + // Restore should make all items visible again buffer.Restore(); var snapshot = new List(); buffer.TakeSnapshot(snapshot); - // Assert Assert.Equal(new[] { 3, 4, 5 }, snapshot); } } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/BasicFormattingTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/BasicFormattingTests.cs index 092ad9c..1b68499 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/BasicFormattingTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/BasicFormattingTests.cs @@ -17,7 +17,6 @@ public void Constructor_InitializesRichTextBoxCorrectly() [Fact] public void Emit_WithScalarValues_FormatsCorrectly() { - // Arrange var template = _parser.Parse("String value: {String}"); var logEvent = new LogEvent( DateTimeOffset.Now, @@ -26,10 +25,8 @@ public void Emit_WithScalarValues_FormatsCorrectly() template, new[] { new LogEventProperty("String", new ScalarValue("test")) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("String value: test", text); } @@ -37,7 +34,6 @@ public void Emit_WithScalarValues_FormatsCorrectly() [Fact] public void Emit_WithDictionaryValue_FormatsCorrectly() { - // Arrange var dict = new Dictionary { ["key1"] = "value1", @@ -59,10 +55,8 @@ public void Emit_WithDictionaryValue_FormatsCorrectly() template, new[] { new LogEventProperty("Dict", dictValue) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Dictionary: {", text); Assert.Contains("[key1]=value1", text); @@ -72,7 +66,6 @@ public void Emit_WithDictionaryValue_FormatsCorrectly() [Fact] public void Emit_WithSequenceValue_FormatsCorrectly() { - // Arrange var array = new object[] { 1, 2, 3, "test" }; var sequenceValue = new SequenceValue(array.Select(x => new ScalarValue(x))); @@ -84,10 +77,8 @@ public void Emit_WithSequenceValue_FormatsCorrectly() template, new[] { new LogEventProperty("Array", sequenceValue) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Array: [", text); Assert.Contains("1, 2, 3, test", text); @@ -96,7 +87,6 @@ public void Emit_WithSequenceValue_FormatsCorrectly() [Fact] public void Emit_WithStructureValue_FormatsCorrectly() { - // Arrange var structureValue = new StructureValue(new[] { new LogEventProperty("Name", new ScalarValue("Test")), @@ -112,10 +102,8 @@ public void Emit_WithStructureValue_FormatsCorrectly() template, new[] { new LogEventProperty("Object", structureValue) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Object: {", text); Assert.Contains("Name=Test", text); @@ -126,7 +114,6 @@ public void Emit_WithStructureValue_FormatsCorrectly() [Fact] public void Emit_WithComplexNestedValue_FormatsCorrectly() { - // Arrange var complex = new StructureValue(new[] { new LogEventProperty("Name", new ScalarValue("Test")), @@ -146,10 +133,8 @@ public void Emit_WithComplexNestedValue_FormatsCorrectly() template, new[] { new LogEventProperty("Complex", complex) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Complex: {", text); Assert.Contains("Name=Test", text); @@ -162,7 +147,6 @@ public void Emit_WithComplexNestedValue_FormatsCorrectly() [Fact] public void Emit_WithSequenceValue_JsonFormatting_FormatsCorrectly() { - // Arrange var array = new object[] { 1, 2, 3, "test" }; var sequenceValue = new SequenceValue(array.Select(x => new ScalarValue(x))); @@ -174,10 +158,8 @@ public void Emit_WithSequenceValue_JsonFormatting_FormatsCorrectly() template, new[] { new LogEventProperty("Array", sequenceValue) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Array: [", text); Assert.Contains("1, 2, 3, \"test\"", text); @@ -186,7 +168,6 @@ public void Emit_WithSequenceValue_JsonFormatting_FormatsCorrectly() [Fact] public void Emit_WithNestedSequenceValue_JsonFormatting_FormatsCorrectly() { - // Arrange var nestedArray = new object[] { new object[] { 1, 2 }, new object[] { 3, 4 } }; var sequenceValue = new SequenceValue(nestedArray.Select(x => new SequenceValue(((object[])x).Select(y => new ScalarValue(y))))); @@ -199,10 +180,8 @@ public void Emit_WithNestedSequenceValue_JsonFormatting_FormatsCorrectly() template, new[] { new LogEventProperty("NestedArray", sequenceValue) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("NestedArray: [", text); Assert.Contains("[[1, 2], [3, 4]]", text); @@ -211,7 +190,6 @@ public void Emit_WithNestedSequenceValue_JsonFormatting_FormatsCorrectly() [Fact] public void Emit_WithEmptySequenceValue_JsonFormatting_FormatsCorrectly() { - // Arrange var emptyArray = new object[] { }; var sequenceValue = new SequenceValue(emptyArray.Select(x => new ScalarValue(x))); @@ -223,10 +201,8 @@ public void Emit_WithEmptySequenceValue_JsonFormatting_FormatsCorrectly() template, new[] { new LogEventProperty("EmptyArray", sequenceValue) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("EmptyArray: []", text); } @@ -234,7 +210,6 @@ public void Emit_WithEmptySequenceValue_JsonFormatting_FormatsCorrectly() [Fact] public void Emit_WithStructureValue_JsonFormatting_FormatsCorrectly() { - // Arrange var structureValue = new StructureValue(new[] { new LogEventProperty("Name", new ScalarValue("Test")), @@ -250,10 +225,8 @@ public void Emit_WithStructureValue_JsonFormatting_FormatsCorrectly() template, new[] { new LogEventProperty("Object", structureValue) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Object: {", text); Assert.Contains("\"Name\": \"Test\"", text); @@ -264,7 +237,6 @@ public void Emit_WithStructureValue_JsonFormatting_FormatsCorrectly() [Fact] public void Emit_WithStructureValueWithTypeTag_JsonFormatting_FormatsCorrectly() { - // Arrange var structureValue = new StructureValue( new[] { @@ -281,10 +253,8 @@ public void Emit_WithStructureValueWithTypeTag_JsonFormatting_FormatsCorrectly() template, new[] { new LogEventProperty("Object", structureValue) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Object: {", text); Assert.Contains("\"Name\": \"Test\"", text); @@ -295,9 +265,7 @@ public void Emit_WithStructureValueWithTypeTag_JsonFormatting_FormatsCorrectly() [Fact] public void Emit_WithEmptyStructureValue_JsonFormatting_FormatsCorrectly() { - // Arrange var structureValue = new StructureValue(Array.Empty()); - var template = _parser.Parse("Object: {@Object:j}"); var logEvent = new LogEvent( DateTimeOffset.Now, @@ -306,10 +274,8 @@ public void Emit_WithEmptyStructureValue_JsonFormatting_FormatsCorrectly() template, new[] { new LogEventProperty("Object", structureValue) }); - // Act _renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Object: {}", text); } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/PropertiesTokenRendererTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/PropertiesTokenRendererTests.cs index 8a3d2a2..1d3bee3 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/PropertiesTokenRendererTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/PropertiesTokenRendererTests.cs @@ -10,7 +10,6 @@ public class PropertiesTokenRendererTests : RichTextBoxSinkTestBase [Fact] public void Render_WithAdditionalProperties_FormatsCorrectly() { - // Arrange var template = _parser.Parse("Message: {Message}"); var outputTemplate = _parser.Parse("Message: {Message} {Properties}"); var token = outputTemplate.Tokens.OfType().Single(t => t.PropertyName == "Properties"); @@ -27,10 +26,8 @@ public void Render_WithAdditionalProperties_FormatsCorrectly() new LogEventProperty("Additional", new ScalarValue("value")) }); - // Act renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Additional=\"value\"", text); Assert.DoesNotContain("Message=\"test\"", text); @@ -39,7 +36,6 @@ public void Render_WithAdditionalProperties_FormatsCorrectly() [Fact] public void Render_WithJsonFormatting_FormatsCorrectly() { - // Arrange var template = _parser.Parse("Message: {Message}"); var outputTemplate = _parser.Parse("Message: {Message} {Properties:j}"); var token = outputTemplate.Tokens.OfType().Single(t => t.PropertyName == "Properties"); @@ -57,10 +53,8 @@ public void Render_WithJsonFormatting_FormatsCorrectly() new LogEventProperty("Boolean", new ScalarValue(true)) }); - // Act renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("\"Number\": 42", text); Assert.Contains("\"Boolean\": true", text); @@ -70,7 +64,6 @@ public void Render_WithJsonFormatting_FormatsCorrectly() [Fact] public void Render_WithNestedProperties_FormatsCorrectly() { - // Arrange var template = _parser.Parse("Message: {Message}"); var outputTemplate = _parser.Parse("Message: {Message} {Properties}"); var token = outputTemplate.Tokens.OfType().Single(t => t.PropertyName == "Properties"); @@ -92,10 +85,8 @@ public void Render_WithNestedProperties_FormatsCorrectly() new LogEventProperty("Nested", nestedStructure) }); - // Act renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Nested={", text); Assert.Contains("NestedValue=\"nested\"", text); @@ -104,7 +95,6 @@ public void Render_WithNestedProperties_FormatsCorrectly() [Fact] public void Render_WithNoAdditionalProperties_FormatsCorrectly() { - // Arrange var template = _parser.Parse("Message: {Message}"); var outputTemplate = _parser.Parse("Message: {Message} {Properties}"); var token = outputTemplate.Tokens.OfType().Single(t => t.PropertyName == "Properties"); @@ -120,10 +110,8 @@ public void Render_WithNoAdditionalProperties_FormatsCorrectly() new LogEventProperty("Message", new ScalarValue("test")) }); - // Act renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Equal("{}", text); } @@ -131,7 +119,6 @@ public void Render_WithNoAdditionalProperties_FormatsCorrectly() [Fact] public void Render_WithPropertiesInOutputTemplate_ExcludesThem() { - // Arrange var template = _parser.Parse("Message: {Message}"); var outputTemplate = _parser.Parse("Message: {Message} {Properties} {Custom}"); var token = outputTemplate.Tokens.OfType().Single(t => t.PropertyName == "Properties"); @@ -149,10 +136,8 @@ public void Render_WithPropertiesInOutputTemplate_ExcludesThem() new LogEventProperty("Additional", new ScalarValue("extra")) }); - // Act renderer.Render(logEvent, _canvas); - // Assert var text = _richTextBox.Text; Assert.Contains("Additional=\"extra\"", text); Assert.DoesNotContain("Custom=\"value\"", text); diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/SinkLifecycleTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/SinkLifecycleTests.cs index 82b8035..ac4d546 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/SinkLifecycleTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/SinkLifecycleTests.cs @@ -22,8 +22,8 @@ public void Dispose_CancelsMessageProcessing() try { - // Act & Assert testSink.Dispose(); + // Give the background thread time to complete Thread.Sleep(100); Assert.Throws(() => testSink.Emit(new LogEvent( diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/SpecialCaseFormattingTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/SpecialCaseFormattingTests.cs index 12e1dbc..007f162 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/SpecialCaseFormattingTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/SpecialCaseFormattingTests.cs @@ -16,7 +16,6 @@ public class SpecialCaseFormattingTests : RichTextBoxSinkTestBase [InlineData("Link", "http://example.com/path", "{Link}", "{Message}", "http://example.com/path")] public void ScalarTypes_SpecialCases_DefaultFormatting_RendersCorrectly(string propertyName, object value, string template, string outputTemplate, string expected) { - // Arrange var scalarValue = value switch { byte[] bytes => new ScalarValue(bytes), @@ -31,98 +30,82 @@ public void ScalarTypes_SpecialCases_DefaultFormatting_RendersCorrectly(string p var prop = new LogEventProperty(propertyName, scalarValue); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse(template), new[] { prop }); - // Act & Assert Assert.Equal(expected, RenderAndGetText(logEvent, outputTemplate, CultureInfo.InvariantCulture)); } [Fact] public void JsonFormatting_ByteArray_RendersCorrectly() { - // Arrange var bytes = new byte[] { 1, 2, 3, 4 }; var byteProp = new LogEventProperty("Bytes", new ScalarValue(bytes)); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Bytes}"), new[] { byteProp }); - // Act & Assert Assert.Equal($"\"{Convert.ToBase64String(bytes)}\"", RenderAndGetText(logEvent, "{Message:j}", CultureInfo.InvariantCulture)); } [Fact] public void JsonFormatting_DateTime_RendersCorrectly() { - // Arrange var dt = new DateTime(2023, 1, 15, 10, 30, 45, DateTimeKind.Utc); var dtProp = new LogEventProperty("Time", new ScalarValue(dt)); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Time}"), new[] { dtProp }); - // Act & Assert Assert.Equal($"\"{dt.ToString("O")}\"", RenderAndGetText(logEvent, "{Message:j}", CultureInfo.InvariantCulture)); } [Fact] public void JsonFormatting_DateTimeOffset_RendersCorrectly() { - // Arrange var dto = new DateTimeOffset(2023, 1, 15, 10, 30, 45, TimeSpan.FromHours(2)); var dtoProp = new LogEventProperty("TimeOffset", new ScalarValue(dto)); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{TimeOffset}"), new[] { dtoProp }); - // Act & Assert Assert.Equal($"\"{dto.ToString("O")}\"", RenderAndGetText(logEvent, "{Message:j}", CultureInfo.InvariantCulture)); } [Fact] public void JsonFormatting_TimeSpan_RendersCorrectly() { - // Arrange var ts = TimeSpan.FromSeconds(12345); var tsProp = new LogEventProperty("Duration", new ScalarValue(ts)); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Duration}"), new[] { tsProp }); - // Act & Assert Assert.Equal($"\"{ts.ToString()}\"", RenderAndGetText(logEvent, "{Message:j}", CultureInfo.InvariantCulture)); } [Fact] public void JsonFormatting_Guid_RendersCorrectly() { - // Arrange var guid = Guid.NewGuid(); var guidProp = new LogEventProperty("Id", new ScalarValue(guid)); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Id}"), new[] { guidProp }); - // Act & Assert Assert.Equal($"\"{guid.ToString("D")}\"", RenderAndGetText(logEvent, "{Message:j}", CultureInfo.InvariantCulture)); } [Fact] public void JsonFormatting_Uri_RendersCorrectly() { - // Arrange var uri = new Uri("http://test.com/path"); var uriProp = new LogEventProperty("Link", new ScalarValue(uri)); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Link}"), new[] { uriProp }); - // Act & Assert Assert.Equal($"\"{uri.ToString()}\"", RenderAndGetText(logEvent, "{Message:j}", CultureInfo.InvariantCulture)); } [Fact] public void JsonFormatting_Decimal_RendersCorrectly() { - // Arrange var decVal = 10.33m; var decProp = new LogEventProperty("Scalar", new ScalarValue(decVal)); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Scalar}"), new[] { decProp }); - // Act & Assert Assert.Equal(decVal.ToString(CultureInfo.InvariantCulture), RenderAndGetText(logEvent, "{Message:j}", CultureInfo.InvariantCulture)); } [Fact] public void OutputTemplate_WithSourceContextWithoutFormat_DoesNotCrash() { - // Arrange - Test for bug fix where {SourceContext} without format would cause exception var sourceContextProp = new LogEventProperty("SourceContext", new ScalarValue("MyApp.Services.UserService")); var template = _parser.Parse("User created successfully"); var logEvent = new LogEvent( @@ -133,20 +116,15 @@ public void OutputTemplate_WithSourceContextWithoutFormat_DoesNotCrash() new[] { sourceContextProp }); var outputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"; - - // Act & Assert - Should not throw an exception var result = RenderAndGetText(logEvent, outputTemplate); - - // Verify the output contains expected parts Assert.Contains("[MyApp.Services.UserService]", result); Assert.Contains("User created successfully", result); - Assert.Contains("INF", result); // Level:u3 formatting + Assert.Contains("INF", result); } [Fact] public void OutputTemplate_WithSourceContextWithFormat_WorksCorrectly() { - // Arrange - Test that format specifiers on SourceContext work correctly var sourceContextProp = new LogEventProperty("SourceContext", new ScalarValue("MyApp.Services.UserService")); var template = _parser.Parse("User login attempt"); var logEvent = new LogEvent( @@ -157,14 +135,10 @@ public void OutputTemplate_WithSourceContextWithFormat_WorksCorrectly() new[] { sourceContextProp }); var outputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext:u}] {Message:lj}{NewLine}{Exception}"; - - // Act & Assert - Should not throw an exception var result = RenderAndGetText(logEvent, outputTemplate); - - // Verify the output contains expected parts with uppercase SourceContext Assert.Contains("[MYAPP.SERVICES.USERSERVICE]", result); Assert.Contains("User login attempt", result); - Assert.Contains("WRN", result); // Level:u3 formatting + Assert.Contains("WRN", result); } [Theory] @@ -179,7 +153,6 @@ public void OutputTemplate_WithSourceContextWithFormat_WorksCorrectly() [InlineData("hello", null, "hello")] public void TextFormatter_WithEmptyValues_DoesNotCrash(string value, string? format, string expected) { - // Act & Assert - Should not throw an exception for empty values var result = TextFormatter.Format(value, format); Assert.Equal(expected, result); } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs index 26b7e07..1bec2f4 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs @@ -78,20 +78,10 @@ public int MaxLogLines public IFormatProvider? FormatProvider { get; } - /// - /// When true, formats JSON values (when using the :j format specifier) with indentation and line breaks for better readability. - /// Defaults to false for compact JSON output. - /// public bool PrettyPrintJson { get; } - /// - /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. - /// public int IndentSize { get; } - /// - /// When true (default), uses spaces for indentation; otherwise uses tabs. - /// public bool UseSpacesForIndent { get; } } } \ No newline at end of file From 84cf276a851d05808819e1d0e7bd10dc431766fa Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 16:34:45 +0100 Subject: [PATCH 08/15] Improve JSON pretty print indentation and validation Refines JSON pretty printing to respect indent size for both spaces and tabs, adds validation to restrict indent size between 1 and 16, and updates related tests and documentation. Also optimizes JSON string escaping and clarifies parameter descriptions. --- .github/workflows/build.yml | 15 ++- Demo/Form1.cs | 4 +- .../Integration/JsonFormattingTests.cs | 22 +++- .../Integration/TokenRendererTests.cs | 4 +- ...extBoxSinkLoggerConfigurationExtensions.cs | 4 +- .../Formatting/JsonValueFormatter.cs | 120 +++++++++--------- .../Formatting/ValueFormatterState.cs | 6 +- .../RichTextBoxSinkOptions.cs | 14 +- 8 files changed, 108 insertions(+), 81 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1085236..41a3a05 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,26 +9,27 @@ on: jobs: build-and-test: runs-on: windows-latest - + steps: - uses: actions/checkout@v4 - + - name: Setup .NET 8.0 uses: actions/setup-dotnet@v5 with: dotnet-version: 8.0.x - + - name: Setup MSBuild uses: microsoft/setup-msbuild@v2 - + - name: Restore dependencies run: dotnet restore - + - name: Build run: dotnet build --no-restore --configuration Release --framework net8.0-windows - + - name: Test with Coverage run: dotnet test --no-build --configuration Release --framework net8.0-windows --collect:"XPlat Code Coverage" --results-directory ./coverage + - name: Generate coverage report uses: danielpalme/ReportGenerator-GitHub-Action@v5 with: @@ -36,12 +37,14 @@ jobs: targetdir: './coverage/report' reporttypes: 'Html;HtmlSummary' title: 'Code Coverage Report' + - name: Upload coverage report uses: actions/upload-artifact@v4 with: name: coverage-report path: './coverage/report' continue-on-error: true + - name: Check coverage threshold run: | $coverage = Select-Xml -Path "./coverage/**/coverage.cobertura.xml" -XPath "//coverage/@line-rate" | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value diff --git a/Demo/Form1.cs b/Demo/Form1.cs index 4edfb45..7946ed7 100644 --- a/Demo/Form1.cs +++ b/Demo/Form1.cs @@ -357,11 +357,11 @@ private void btnPrettyPrint_Click(object sender, EventArgs e) { _prettyPrintJson = !_prettyPrintJson; btnPrettyPrint.Text = _prettyPrintJson ? "Disable Pretty Print" : "Enable Pretty Print"; - + // Recreate the sink and logger with new pretty print setting CloseAndFlush(); Initialize(); - + Log.Information("Pretty print JSON: {PrettyPrint}", _prettyPrintJson); } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs index 360ed82..d5b38d2 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs @@ -220,7 +220,7 @@ public void PrettyPrintJson_UsesTabsWhenConfigured() useSpacesForIndent: false); var result = RenderAndGetText(logEvent, "{Message:l}", options); - var expected = "{\n\t\"Id\": 123,\n\t\"$type\": \"MyObj\"\n}"; + var expected = "{\n\t\t\t\t\"Id\": 123,\n\t\t\t\t\"$type\": \"MyObj\"\n}"; Assert.Equal(expected, result); } @@ -244,6 +244,26 @@ public void PrettyPrintJson_RespectsIndentSize() Assert.Equal(expected, result); } + [Fact] + public void PrettyPrintJson_RespectsIndentSizeWithTabs() + { + var prop = new LogEventProperty("Test", new StructureValue(new[] + { + new LogEventProperty("Id", new ScalarValue(123)) + }, "MyObj")); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Test:j}"), new[] { prop }); + + var options = new RichTextBoxSinkOptions( + theme: _defaultTheme, + prettyPrintJson: true, + indentSize: 2, + useSpacesForIndent: false); + + var result = RenderAndGetText(logEvent, "{Message:l}", options); + var expected = "{\n\t\t\"Id\": 123,\n\t\t\"$type\": \"MyObj\"\n}"; + Assert.Equal(expected, result); + } + [Fact] public void CompactJson_StillWorksByDefault() { diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs index 953713d..544d049 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs @@ -10,12 +10,12 @@ public class TokenRendererTests : RichTextBoxSinkTestBase [Fact] public void ExceptionTokenRenderer_RendersExceptionWithStackFrames() { - Exception? exception = null; + Exception? exception; try { throw new InvalidOperationException("Test exception"); } - catch (Exception ex) + catch (InvalidOperationException ex) { exception = ex; } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs index 70a0119..3490771 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs @@ -46,7 +46,7 @@ public static class RichTextBoxSinkLoggerConfigurationExtensions /// Minimum log level for events to be written. /// Optional switch to change the minimum log level at runtime. /// If true, formats JSON values with indentation and line breaks. Defaults to false. - /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16. /// If true (default), uses spaces for indentation; otherwise uses tabs. /// The logger configuration, for chaining. public static LoggerConfiguration RichTextBox( @@ -85,7 +85,7 @@ public static LoggerConfiguration RichTextBox( /// Minimum log level for events to be written. /// Optional switch to change the minimum log level at runtime. /// If true, formats JSON values with indentation and line breaks. Defaults to false. - /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16. /// If true (default), uses spaces for indentation; otherwise uses tabs. /// The logger configuration, for chaining. public static LoggerConfiguration RichTextBox( diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs index e93f01f..741db1f 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs @@ -35,6 +35,7 @@ public class JsonValueFormatter : ValueFormatter private readonly bool _useSpacesForIndent; private readonly StringBuilder _literalBuilder = new(64); private readonly StringBuilder _scalarBuilder = new(); + private readonly StringBuilder _jsonStringBuilder = new(); public JsonValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrint = false, int indentSize = 4, bool useSpacesForIndent = true) : base(theme, formatProvider) { @@ -365,82 +366,75 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas) } } - /// - /// Write a valid JSON string literal, escaping as necessary. - /// Optimized version that avoids unnecessary string operations. - /// - /// The string value to write. - public static string GetQuotedJsonString(string str) + private string GetQuotedJsonString(string str) { - using (var output = new StringWriter()) - { - output.Write('\"'); + _jsonStringBuilder.Clear(); + _jsonStringBuilder.Append('\"'); - var cleanSegmentStart = 0; - var anyEscaped = false; + var cleanSegmentStart = 0; + var anyEscaped = false; - for (var i = 0; i < str.Length; ++i) + for (var i = 0; i < str.Length; ++i) + { + var c = str[i]; + if (c < (char)32 || c == '\\' || c == '"') { - var c = str[i]; - if (c < (char)32 || c == '\\' || c == '"') - { - anyEscaped = true; - - if (i > cleanSegmentStart) - { - output.Write(str.Substring(cleanSegmentStart, i - cleanSegmentStart)); - } - cleanSegmentStart = i + 1; - - switch (c) - { - case '"': - output.Write("\\\""); - break; - - case '\\': - output.Write("\\\\"); - break; - - case '\n': - output.Write("\\n"); - break; - - case '\r': - output.Write("\\r"); - break; - - case '\f': - output.Write("\\f"); - break; + anyEscaped = true; - case '\t': - output.Write("\\t"); - break; - - default: - output.Write("\\u"); - output.Write(((int)c).ToString("X4")); - break; - } + if (i > cleanSegmentStart) + { + _jsonStringBuilder.Append(str, cleanSegmentStart, i - cleanSegmentStart); } - } + cleanSegmentStart = i + 1; - if (anyEscaped) - { - if (cleanSegmentStart < str.Length) + switch (c) { - output.Write(str.Substring(cleanSegmentStart)); + case '"': + _jsonStringBuilder.Append("\\\""); + break; + + case '\\': + _jsonStringBuilder.Append("\\\\"); + break; + + case '\n': + _jsonStringBuilder.Append("\\n"); + break; + + case '\r': + _jsonStringBuilder.Append("\\r"); + break; + + case '\f': + _jsonStringBuilder.Append("\\f"); + break; + + case '\t': + _jsonStringBuilder.Append("\\t"); + break; + + default: + _jsonStringBuilder.Append("\\u"); + _jsonStringBuilder.Append(((int)c).ToString("X4")); + break; } } - else + } + + if (anyEscaped) + { + if (cleanSegmentStart < str.Length) { - output.Write(str); + _jsonStringBuilder.Append(str, cleanSegmentStart, str.Length - cleanSegmentStart); } - - output.Write('\"'); - return output.ToString(); } + else + { + _jsonStringBuilder.Append(str); + } + + _jsonStringBuilder.Append('\"'); + return _jsonStringBuilder.ToString(); } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs index ced0e93..c9ff880 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs @@ -63,10 +63,10 @@ public string GetIndentation() return string.Empty; } - var totalSpaces = IndentLevel * IndentSize; + var totalIndentUnits = IndentLevel * IndentSize; return UseSpacesForIndent - ? new string(' ', totalSpaces) - : new string('\t', IndentLevel); + ? new string(' ', totalIndentUnits) + : new string('\t', totalIndentUnits); } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs index 1bec2f4..57a810d 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs @@ -26,6 +26,7 @@ public class RichTextBoxSinkOptions { private const string DefaultOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"; private int _maxLogLines; + private int _indentSize; /// /// Creates a new collection of options that control the behavior and appearance of a @@ -37,7 +38,7 @@ public class RichTextBoxSinkOptions /// Serilog output template that controls textual formatting of each log event. /// Optional culture-specific or custom formatting provider used when rendering scalar values; null for the invariant culture. /// When true, formats JSON values with indentation and line breaks for better readability. Defaults to false. - /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. + /// Number of indentation units per indentation level when pretty printing JSON. When using spaces, this is the number of spaces; when using tabs, this is the number of tabs. Defaults to 4. Must be between 1 and 16. /// When true (default), uses spaces for indentation; otherwise uses tabs. public RichTextBoxSinkOptions( Theme theme, @@ -80,7 +81,16 @@ public int MaxLogLines public bool PrettyPrintJson { get; } - public int IndentSize { get; } + public int IndentSize + { + get => _indentSize; + private set => _indentSize = value switch + { + < 1 => 1, + > 16 => 16, + _ => value + }; + } public bool UseSpacesForIndent { get; } } From 0ebe3c2a072657d8f79bc16f0a6d42112fb96d8f Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 16:45:38 +0100 Subject: [PATCH 09/15] Refactor JSON indentation to use spaces only Replaces the previous indentSize and useSpacesForIndent options with a single spacesPerIndent parameter, enforcing space-based indentation for pretty-printed JSON. Updates all related constructors, method signatures, and tests to reflect this change, simplifying configuration and code paths. --- .../Integration/JsonFormattingTests.cs | 48 ++++--------------- ...extBoxSinkLoggerConfigurationExtensions.cs | 17 +++---- .../Formatting/ValueFormatterState.cs | 19 +++----- .../Rendering/MessageTemplateTokenRenderer.cs | 4 +- .../Rendering/PropertiesTokenRenderer.cs | 4 +- .../RichTextBoxSinkOptions.cs | 19 +++----- 6 files changed, 34 insertions(+), 77 deletions(-) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs index d5b38d2..7fbd12a 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs @@ -107,8 +107,7 @@ public void PrettyPrintJson_FormatsNestedObjectsWithIndentation() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - indentSize: 4, - useSpacesForIndent: true); + spacesPerIndent: 4, var result = RenderAndGetText(logEvent, "{Message:l}", options); var expected = "{\n \"Id\": 123,\n \"Name\": \"test\",\n \"$type\": \"MyObj\"\n}"; @@ -129,8 +128,7 @@ public void PrettyPrintJson_FormatsArraysWithIndentation() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - indentSize: 4, - useSpacesForIndent: true); + spacesPerIndent: 4, var result = RenderAndGetText(logEvent, "{Message:l}", options); var expected = "[\n 1,\n 2,\n 3\n]"; @@ -151,8 +149,7 @@ public void PrettyPrintJson_FormatsDictionariesWithIndentation() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - indentSize: 4, - useSpacesForIndent: true); + spacesPerIndent: 4, var result = RenderAndGetText(logEvent, "{Message:l}", options); var expected = "{\n \"a\": 1,\n \"b\": \"hello\"\n}"; @@ -176,8 +173,7 @@ public void PrettyPrintJson_FormatsNestedStructures() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - indentSize: 4, - useSpacesForIndent: true); + spacesPerIndent: 4, var result = RenderAndGetText(logEvent, "{Message:l}", options); var expected = "{\n \"Inner\": {\n \"Value\": 42,\n \"$type\": \"Inner\"\n },\n \"Name\": \"test\",\n \"$type\": \"Outer\"\n}"; @@ -195,8 +191,7 @@ public void PrettyPrintJson_EmptyCollectionsFormatCorrectly() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - indentSize: 4, - useSpacesForIndent: true); + spacesPerIndent: 4, var result = RenderAndGetText(logEvent, "{Message:l}", options); Assert.Contains("Array: []", result); @@ -216,8 +211,7 @@ public void PrettyPrintJson_UsesTabsWhenConfigured() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - indentSize: 4, - useSpacesForIndent: false); + spacesPerIndent: 4, var result = RenderAndGetText(logEvent, "{Message:l}", options); var expected = "{\n\t\t\t\t\"Id\": 123,\n\t\t\t\t\"$type\": \"MyObj\"\n}"; @@ -225,7 +219,7 @@ public void PrettyPrintJson_UsesTabsWhenConfigured() } [Fact] - public void PrettyPrintJson_RespectsIndentSize() + public void PrettyPrintJson_RespectsSpacesPerIndent() { var prop = new LogEventProperty("Test", new StructureValue(new[] { @@ -236,33 +230,13 @@ public void PrettyPrintJson_RespectsIndentSize() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - indentSize: 2, - useSpacesForIndent: true); + spacesPerIndent: 2); var result = RenderAndGetText(logEvent, "{Message:l}", options); var expected = "{\n \"Id\": 123,\n \"$type\": \"MyObj\"\n}"; Assert.Equal(expected, result); } - [Fact] - public void PrettyPrintJson_RespectsIndentSizeWithTabs() - { - var prop = new LogEventProperty("Test", new StructureValue(new[] - { - new LogEventProperty("Id", new ScalarValue(123)) - }, "MyObj")); - var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Test:j}"), new[] { prop }); - - var options = new RichTextBoxSinkOptions( - theme: _defaultTheme, - prettyPrintJson: true, - indentSize: 2, - useSpacesForIndent: false); - - var result = RenderAndGetText(logEvent, "{Message:l}", options); - var expected = "{\n\t\t\"Id\": 123,\n\t\t\"$type\": \"MyObj\"\n}"; - Assert.Equal(expected, result); - } [Fact] public void CompactJson_StillWorksByDefault() @@ -311,8 +285,7 @@ public void PrettyPrintJson_DictionaryWithNullKey() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - indentSize: 4, - useSpacesForIndent: true); + spacesPerIndent: 4, var result = RenderAndGetText(logEvent, "{Message:l}", options); Assert.Contains("\"null\"", result); @@ -332,8 +305,7 @@ public void PrettyPrintJson_DictionaryWithNonStringKey() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - indentSize: 4, - useSpacesForIndent: true); + spacesPerIndent: 4, var result = RenderAndGetText(logEvent, "{Message:l}", options); Assert.Contains("\"123\"", result); diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs index 3490771..c9ac8f5 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs @@ -46,8 +46,7 @@ public static class RichTextBoxSinkLoggerConfigurationExtensions /// Minimum log level for events to be written. /// Optional switch to change the minimum log level at runtime. /// If true, formats JSON values with indentation and line breaks. Defaults to false. - /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16. - /// If true (default), uses spaces for indentation; otherwise uses tabs. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16. /// The logger configuration, for chaining. public static LoggerConfiguration RichTextBox( this LoggerSinkConfiguration sinkConfiguration, @@ -61,12 +60,11 @@ public static LoggerConfiguration RichTextBox( LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose, LoggingLevelSwitch? levelSwitch = null, bool prettyPrintJson = false, - int indentSize = 4, - bool useSpacesForIndent = true) + int spacesPerIndent = 4) { var appliedTheme = theme ?? ThemePresets.Literate; var appliedFormatProvider = formatProvider ?? CultureInfo.InvariantCulture; - var options = new RichTextBoxSinkOptions(appliedTheme, autoScroll, maxLogLines, outputTemplate, appliedFormatProvider, prettyPrintJson, indentSize, useSpacesForIndent); + var options = new RichTextBoxSinkOptions(appliedTheme, autoScroll, maxLogLines, outputTemplate, appliedFormatProvider, prettyPrintJson, spacesPerIndent); var renderer = new TemplateRenderer(appliedTheme, outputTemplate, appliedFormatProvider, options); richTextBoxSink = new RichTextBoxSink(richTextBoxControl, options, renderer); return sinkConfiguration.Sink(richTextBoxSink, minimumLogEventLevel, levelSwitch); @@ -85,8 +83,7 @@ public static LoggerConfiguration RichTextBox( /// Minimum log level for events to be written. /// Optional switch to change the minimum log level at runtime. /// If true, formats JSON values with indentation and line breaks. Defaults to false. - /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16. - /// If true (default), uses spaces for indentation; otherwise uses tabs. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16. /// The logger configuration, for chaining. public static LoggerConfiguration RichTextBox( this LoggerSinkConfiguration sinkConfiguration, @@ -99,8 +96,7 @@ public static LoggerConfiguration RichTextBox( LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose, LoggingLevelSwitch? levelSwitch = null, bool prettyPrintJson = false, - int indentSize = 4, - bool useSpacesForIndent = true) + int spacesPerIndent = 4) { return RichTextBox( sinkConfiguration, @@ -114,8 +110,7 @@ public static LoggerConfiguration RichTextBox( minimumLogEventLevel, levelSwitch, prettyPrintJson, - indentSize, - useSpacesForIndent); + spacesPerIndent); } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs index c9ff880..10ae8d6 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs @@ -22,14 +22,13 @@ namespace Serilog.Sinks.RichTextBoxForms.Formatting { public readonly struct ValueFormatterState { - public ValueFormatterState(IRtfCanvas canvas, string format, bool isLiteral, int indentLevel = 0, bool useSpacesForIndent = true, int indentSize = 4, bool isTopLevel = true) + public ValueFormatterState(IRtfCanvas canvas, string format, bool isLiteral, int indentLevel = 0, int spacesPerIndent = 4, bool isTopLevel = true) { Canvas = canvas; Format = format; IsLiteral = isLiteral; IndentLevel = indentLevel; - UseSpacesForIndent = useSpacesForIndent; - IndentSize = indentSize; + SpacesPerIndent = spacesPerIndent; IsTopLevel = isTopLevel; } @@ -37,23 +36,22 @@ public ValueFormatterState(IRtfCanvas canvas, string format, bool isLiteral, int public bool IsLiteral { get; } public IRtfCanvas Canvas { get; } public int IndentLevel { get; } - public bool UseSpacesForIndent { get; } - public int IndentSize { get; } + public int SpacesPerIndent { get; } public bool IsTopLevel { get; } public ValueFormatterState Next(string? format = null) { - return new ValueFormatterState(Canvas, format ?? Format, IsLiteral, IndentLevel, UseSpacesForIndent, IndentSize, false); + return new ValueFormatterState(Canvas, format ?? Format, IsLiteral, IndentLevel, SpacesPerIndent, false); } public ValueFormatterState ToIndentUp() { - return new ValueFormatterState(Canvas, Format, IsLiteral, IndentLevel + 1, UseSpacesForIndent, IndentSize, false); + return new ValueFormatterState(Canvas, Format, IsLiteral, IndentLevel + 1, SpacesPerIndent, false); } public ValueFormatterState ToIndentDown() { - return new ValueFormatterState(Canvas, Format, IsLiteral, IndentLevel > 0 ? IndentLevel - 1 : 0, UseSpacesForIndent, IndentSize, false); + return new ValueFormatterState(Canvas, Format, IsLiteral, IndentLevel > 0 ? IndentLevel - 1 : 0, SpacesPerIndent, false); } public string GetIndentation() @@ -63,10 +61,7 @@ public string GetIndentation() return string.Empty; } - var totalIndentUnits = IndentLevel * IndentSize; - return UseSpacesForIndent - ? new string(' ', totalIndentUnits) - : new string('\t', totalIndentUnits); + return new string(' ', IndentLevel * SpacesPerIndent); } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs index 567c4c0..997090d 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs @@ -35,8 +35,8 @@ public MessageTemplateTokenRenderer(Theme theme, PropertyToken token, IFormatPro var isJson = token.Format?.Contains("j") == true; ValueFormatter valueFormatter = isJson - ? new JsonValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.IndentSize ?? 4, options?.UseSpacesForIndent ?? true) - : new DisplayValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.IndentSize ?? 4, options?.UseSpacesForIndent ?? true); + ? new JsonValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.SpacesPerIndent ?? 4, true) + : new DisplayValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.SpacesPerIndent ?? 4, true); _renderer = new MessageTemplateRenderer(theme, valueFormatter, isLiteral); } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs index 845f862..f7d4a6e 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs @@ -35,8 +35,8 @@ public class PropertiesTokenRenderer : ITokenRenderer public PropertiesTokenRenderer(Theme theme, PropertyToken token, MessageTemplate outputTemplate, IFormatProvider? formatProvider, RichTextBoxSinkOptions? options = null) { _valueFormatter = token.Format?.Contains("j") == true - ? new JsonValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.IndentSize ?? 4, options?.UseSpacesForIndent ?? true) - : new DisplayValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.IndentSize ?? 4, options?.UseSpacesForIndent ?? true); + ? new JsonValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.SpacesPerIndent ?? 4, true) + : new DisplayValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.SpacesPerIndent ?? 4, true); _outputTemplateProperties = new HashSet( outputTemplate.Tokens.OfType().Select(p => p.PropertyName)); diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs index 57a810d..2f4c823 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs @@ -26,7 +26,7 @@ public class RichTextBoxSinkOptions { private const string DefaultOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"; private int _maxLogLines; - private int _indentSize; + private int _spacesPerIndent; /// /// Creates a new collection of options that control the behavior and appearance of a @@ -38,8 +38,7 @@ public class RichTextBoxSinkOptions /// Serilog output template that controls textual formatting of each log event. /// Optional culture-specific or custom formatting provider used when rendering scalar values; null for the invariant culture. /// When true, formats JSON values with indentation and line breaks for better readability. Defaults to false. - /// Number of indentation units per indentation level when pretty printing JSON. When using spaces, this is the number of spaces; when using tabs, this is the number of tabs. Defaults to 4. Must be between 1 and 16. - /// When true (default), uses spaces for indentation; otherwise uses tabs. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16. public RichTextBoxSinkOptions( Theme theme, bool autoScroll = true, @@ -47,8 +46,7 @@ public RichTextBoxSinkOptions( string outputTemplate = DefaultOutputTemplate, IFormatProvider? formatProvider = null, bool prettyPrintJson = false, - int indentSize = 4, - bool useSpacesForIndent = true) + int spacesPerIndent = 4) { AutoScroll = autoScroll; Theme = theme; @@ -56,8 +54,7 @@ public RichTextBoxSinkOptions( OutputTemplate = outputTemplate; FormatProvider = formatProvider ?? CultureInfo.InvariantCulture; PrettyPrintJson = prettyPrintJson; - IndentSize = indentSize; - UseSpacesForIndent = useSpacesForIndent; + SpacesPerIndent = spacesPerIndent; } public bool AutoScroll { get; set; } @@ -81,17 +78,15 @@ public int MaxLogLines public bool PrettyPrintJson { get; } - public int IndentSize + public int SpacesPerIndent { - get => _indentSize; - private set => _indentSize = value switch + get => _spacesPerIndent; + private set => _spacesPerIndent = value switch { < 1 => 1, > 16 => 16, _ => value }; } - - public bool UseSpacesForIndent { get; } } } \ No newline at end of file From ec5845ef88badc1f9d30bf6f76ad38daf8fbcc0d Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 16:51:03 +0100 Subject: [PATCH 10/15] Remove tab-based JSON pretty printing support Removed the option to use tabs for JSON pretty printing and standardized on spaces for indentation. Updated constructors and internal logic in DisplayValueFormatter and JsonValueFormatter to use spacesPerIndent only. Also removed the related test case from JsonFormattingTests. --- .../Integration/JsonFormattingTests.cs | 32 ++++--------------- .../Formatting/DisplayValueFormatter.cs | 14 ++++---- .../Formatting/JsonValueFormatter.cs | 10 +++--- 3 files changed, 17 insertions(+), 39 deletions(-) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs index 7fbd12a..42e8891 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs @@ -107,7 +107,7 @@ public void PrettyPrintJson_FormatsNestedObjectsWithIndentation() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - spacesPerIndent: 4, + spacesPerIndent: 4); var result = RenderAndGetText(logEvent, "{Message:l}", options); var expected = "{\n \"Id\": 123,\n \"Name\": \"test\",\n \"$type\": \"MyObj\"\n}"; @@ -128,7 +128,7 @@ public void PrettyPrintJson_FormatsArraysWithIndentation() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - spacesPerIndent: 4, + spacesPerIndent: 4); var result = RenderAndGetText(logEvent, "{Message:l}", options); var expected = "[\n 1,\n 2,\n 3\n]"; @@ -149,7 +149,7 @@ public void PrettyPrintJson_FormatsDictionariesWithIndentation() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - spacesPerIndent: 4, + spacesPerIndent: 4); var result = RenderAndGetText(logEvent, "{Message:l}", options); var expected = "{\n \"a\": 1,\n \"b\": \"hello\"\n}"; @@ -173,7 +173,7 @@ public void PrettyPrintJson_FormatsNestedStructures() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - spacesPerIndent: 4, + spacesPerIndent: 4); var result = RenderAndGetText(logEvent, "{Message:l}", options); var expected = "{\n \"Inner\": {\n \"Value\": 42,\n \"$type\": \"Inner\"\n },\n \"Name\": \"test\",\n \"$type\": \"Outer\"\n}"; @@ -191,7 +191,7 @@ public void PrettyPrintJson_EmptyCollectionsFormatCorrectly() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - spacesPerIndent: 4, + spacesPerIndent: 4); var result = RenderAndGetText(logEvent, "{Message:l}", options); Assert.Contains("Array: []", result); @@ -199,24 +199,6 @@ public void PrettyPrintJson_EmptyCollectionsFormatCorrectly() Assert.Contains("Object: {}", result); } - [Fact] - public void PrettyPrintJson_UsesTabsWhenConfigured() - { - var prop = new LogEventProperty("Test", new StructureValue(new[] - { - new LogEventProperty("Id", new ScalarValue(123)) - }, "MyObj")); - var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Test:j}"), new[] { prop }); - - var options = new RichTextBoxSinkOptions( - theme: _defaultTheme, - prettyPrintJson: true, - spacesPerIndent: 4, - - var result = RenderAndGetText(logEvent, "{Message:l}", options); - var expected = "{\n\t\t\t\t\"Id\": 123,\n\t\t\t\t\"$type\": \"MyObj\"\n}"; - Assert.Equal(expected, result); - } [Fact] public void PrettyPrintJson_RespectsSpacesPerIndent() @@ -285,7 +267,7 @@ public void PrettyPrintJson_DictionaryWithNullKey() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - spacesPerIndent: 4, + spacesPerIndent: 4); var result = RenderAndGetText(logEvent, "{Message:l}", options); Assert.Contains("\"null\"", result); @@ -305,7 +287,7 @@ public void PrettyPrintJson_DictionaryWithNonStringKey() var options = new RichTextBoxSinkOptions( theme: _defaultTheme, prettyPrintJson: true, - spacesPerIndent: 4, + spacesPerIndent: 4); var result = RenderAndGetText(logEvent, "{Message:l}", options); Assert.Contains("\"123\"", result); diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs index 59c24d3..3df3364 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs @@ -30,18 +30,16 @@ public class DisplayValueFormatter : ValueFormatter { private readonly IFormatProvider? _formatProvider; private readonly bool _prettyPrintJson; - private readonly int _indentSize; - private readonly bool _useSpacesForIndent; + private readonly int _spacesPerIndent; private readonly StringBuilder _scalarBuilder = new(); private readonly StringBuilder _literalBuilder = new(64); private JsonValueFormatter? _jsonValueFormatter; - public DisplayValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrintJson = false, int indentSize = 4, bool useSpacesForIndent = true) : base(theme, formatProvider) + public DisplayValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrintJson = false, int spacesPerIndent = 4, bool useSpacesForIndent = true) : base(theme, formatProvider) { _formatProvider = formatProvider; _prettyPrintJson = prettyPrintJson; - _indentSize = indentSize; - _useSpacesForIndent = useSpacesForIndent; + _spacesPerIndent = spacesPerIndent; } private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas, string? format, bool isLiteral) @@ -106,7 +104,7 @@ protected override bool VisitDictionaryValue(ValueFormatterState state, Dictiona { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _indentSize, _useSpacesForIndent); + _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent, true); _jsonValueFormatter.Format(dictionary, state.Canvas, state.Format, state.IsLiteral); return true; } @@ -142,7 +140,7 @@ protected override bool VisitSequenceValue(ValueFormatterState state, SequenceVa { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _indentSize, _useSpacesForIndent); + _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent, true); _jsonValueFormatter.Format(sequence, state.Canvas, state.Format, state.IsLiteral); return true; } @@ -169,7 +167,7 @@ protected override bool VisitStructureValue(ValueFormatterState state, Structure { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _indentSize, _useSpacesForIndent); + _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent, true); _jsonValueFormatter.Format(structure, state.Canvas, state.Format, state.IsLiteral); return true; } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs index 741db1f..aa84d7d 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs @@ -31,23 +31,21 @@ public class JsonValueFormatter : ValueFormatter { private readonly IFormatProvider? _formatProvider; private readonly bool _prettyPrint; - private readonly int _indentSize; - private readonly bool _useSpacesForIndent; + private readonly int _spacesPerIndent; private readonly StringBuilder _literalBuilder = new(64); private readonly StringBuilder _scalarBuilder = new(); private readonly StringBuilder _jsonStringBuilder = new(); - public JsonValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrint = false, int indentSize = 4, bool useSpacesForIndent = true) : base(theme, formatProvider) + public JsonValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrint = false, int spacesPerIndent = 4, bool useSpacesForIndent = true) : base(theme, formatProvider) { _formatProvider = formatProvider; _prettyPrint = prettyPrint; - _indentSize = indentSize; - _useSpacesForIndent = useSpacesForIndent; + _spacesPerIndent = spacesPerIndent; } protected override ValueFormatterState CreateInitialState(IRtfCanvas canvas, string format, bool isLiteral) { - return new ValueFormatterState(canvas, format, isLiteral, 0, _useSpacesForIndent, _indentSize, true); + return new ValueFormatterState(canvas, format, isLiteral, 0, _spacesPerIndent, true); } protected override bool VisitScalarValue(ValueFormatterState state, ScalarValue scalar) From 9a1db2724014e85cd1cd33a54ad9d7b600aabe84 Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 17:12:18 +0100 Subject: [PATCH 11/15] Add configurable JSON indent size to demo UI Introduces a numeric up/down control in the demo form to allow users to set the number of spaces per indentation level for pretty-printed JSON (0-16, default 2). Updates the logger and sink configuration to use the selected indent size. Also changes the default indent from 4 to 2 in the sink options and extension method. --- Demo/Form1.Designer.cs | 49 +++++++++++++++++-- Demo/Form1.cs | 36 +++++++++++++- ...extBoxSinkLoggerConfigurationExtensions.cs | 4 +- .../RichTextBoxSinkOptions.cs | 6 +-- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/Demo/Form1.Designer.cs b/Demo/Form1.Designer.cs index 890a237..2604634 100644 --- a/Demo/Form1.Designer.cs +++ b/Demo/Form1.Designer.cs @@ -46,16 +46,19 @@ private void InitializeComponent() this.btnStructure = new System.Windows.Forms.ToolStripButton(); this.btnComplex = new System.Windows.Forms.ToolStripButton(); this.toolStripSeparator8 = new System.Windows.Forms.ToolStripSeparator(); + this.btnClear = new System.Windows.Forms.ToolStripButton(); + this.btnRestore = new System.Windows.Forms.ToolStripButton(); + this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); this.btnDispose = new System.Windows.Forms.ToolStripButton(); this.btnReset = new System.Windows.Forms.ToolStripButton(); this.toolStripSeparator6 = new System.Windows.Forms.ToolStripSeparator(); this.btnAutoScroll = new System.Windows.Forms.ToolStripButton(); this.btnPrettyPrint = new System.Windows.Forms.ToolStripButton(); + this.toolStripLabelIndent = new System.Windows.Forms.ToolStripLabel(); + this.numericUpDownSpacesPerIndent = new System.Windows.Forms.NumericUpDown(); + this.toolStripControlHostIndent = new System.Windows.Forms.ToolStripControlHost(this.numericUpDownSpacesPerIndent); this.panel1 = new System.Windows.Forms.Panel(); this.richTextBox1 = new System.Windows.Forms.RichTextBox(); - this.btnRestore = new System.Windows.Forms.ToolStripButton(); - this.btnClear = new System.Windows.Forms.ToolStripButton(); - this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); this.toolStrip1.SuspendLayout(); this.toolStrip2.SuspendLayout(); this.panel1.SuspendLayout(); @@ -176,7 +179,9 @@ private void InitializeComponent() this.btnReset, this.toolStripSeparator6, this.btnAutoScroll, - this.btnPrettyPrint}); + this.btnPrettyPrint, + this.toolStripLabelIndent, + this.toolStripControlHostIndent}); this.toolStrip2.Location = new System.Drawing.Point(0, 25); this.toolStrip2.Name = "toolStrip2"; this.toolStrip2.Padding = new System.Windows.Forms.Padding(3, 0, 3, 0); @@ -280,6 +285,39 @@ private void InitializeComponent() this.btnPrettyPrint.ToolTipText = "Toggle pretty-printed JSON formatting"; this.btnPrettyPrint.Click += new System.EventHandler(this.btnPrettyPrint_Click); // + // toolStripLabelIndent + // + this.toolStripLabelIndent.Name = "toolStripLabelIndent"; + this.toolStripLabelIndent.Size = new System.Drawing.Size(38, 22); + this.toolStripLabelIndent.Text = "Indent:"; + // + // numericUpDownSpacesPerIndent + // + this.numericUpDownSpacesPerIndent.Minimum = new decimal(new int[] { + 0, + 0, + 0, + 0}); + this.numericUpDownSpacesPerIndent.Maximum = new decimal(new int[] { + 16, + 0, + 0, + 0}); + this.numericUpDownSpacesPerIndent.Value = new decimal(new int[] { + 2, + 0, + 0, + 0}); + this.numericUpDownSpacesPerIndent.Width = 50; + this.numericUpDownSpacesPerIndent.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.numericUpDownSpacesPerIndent.ValueChanged += new System.EventHandler(this.numericUpDownSpacesPerIndent_ValueChanged); + // + // toolStripControlHostIndent + // + this.toolStripControlHostIndent.Name = "toolStripControlHostIndent"; + this.toolStripControlHostIndent.Size = new System.Drawing.Size(50, 22); + this.toolStripControlHostIndent.ToolTipText = "Number of spaces per indentation level (0-16)"; + // // panel1 // this.panel1.Controls.Add(this.richTextBox1); @@ -375,6 +413,9 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripSeparator toolStripSeparator6; private System.Windows.Forms.ToolStripButton btnAutoScroll; private System.Windows.Forms.ToolStripButton btnPrettyPrint; + private System.Windows.Forms.ToolStripLabel toolStripLabelIndent; + private System.Windows.Forms.NumericUpDown numericUpDownSpacesPerIndent; + private System.Windows.Forms.ToolStripControlHost toolStripControlHostIndent; private System.Windows.Forms.ToolStripButton btnRestore; private System.Windows.Forms.ToolStripButton btnClear; private System.Windows.Forms.ToolStripSeparator toolStripSeparator2; diff --git a/Demo/Form1.cs b/Demo/Form1.cs index 7946ed7..f10bb41 100644 --- a/Demo/Form1.cs +++ b/Demo/Form1.cs @@ -36,6 +36,8 @@ public partial class Form1 : Form private RichTextBoxSink? _sink; private bool _toolbarsVisible = true; private bool _prettyPrintJson = false; + private int _spacesPerIndent = 2; + private bool _updatingIndent = false; public Form1() { @@ -49,7 +51,8 @@ private void Initialize() theme: ThemePresets.Literate, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}", formatProvider: new CultureInfo("en-US"), - prettyPrintJson: _prettyPrintJson); + prettyPrintJson: _prettyPrintJson, + spacesPerIndent: _spacesPerIndent); _sink = new RichTextBoxSink(richTextBox1, _options); Log.Logger = new LoggerConfiguration() @@ -80,6 +83,14 @@ private void Initialize() Log.Debug("Started logger."); btnDispose.Enabled = true; btnPrettyPrint.Text = _prettyPrintJson ? "Disable Pretty Print" : "Enable Pretty Print"; + + // Update the numeric up/down control without triggering events + _updatingIndent = true; + if (numericUpDownSpacesPerIndent.Value != _spacesPerIndent) + { + numericUpDownSpacesPerIndent.Value = _spacesPerIndent; + } + _updatingIndent = false; } private void Form1_Load(object sender, EventArgs e) @@ -365,6 +376,29 @@ private void btnPrettyPrint_Click(object sender, EventArgs e) Log.Information("Pretty print JSON: {PrettyPrint}", _prettyPrintJson); } + private void numericUpDownSpacesPerIndent_ValueChanged(object sender, EventArgs e) + { + // Prevent recursive calls when we're updating the value programmatically + if (_updatingIndent) + { + return; + } + + var newValue = (int)numericUpDownSpacesPerIndent.Value; + if (newValue == _spacesPerIndent) + { + return; + } + + _spacesPerIndent = newValue; + + // Recreate the sink and logger with new indent size + CloseAndFlush(); + Initialize(); + + Log.Information("Spaces per indent changed to: {SpacesPerIndent}", _spacesPerIndent); + } + private void Form1_KeyDown(object sender, KeyEventArgs e) { if (e.Control && e.KeyCode == Keys.T) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs index c9ac8f5..7986dbe 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs @@ -46,7 +46,7 @@ public static class RichTextBoxSinkLoggerConfigurationExtensions /// Minimum log level for events to be written. /// Optional switch to change the minimum log level at runtime. /// If true, formats JSON values with indentation and line breaks. Defaults to false. - /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 2. /// The logger configuration, for chaining. public static LoggerConfiguration RichTextBox( this LoggerSinkConfiguration sinkConfiguration, @@ -60,7 +60,7 @@ public static LoggerConfiguration RichTextBox( LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose, LoggingLevelSwitch? levelSwitch = null, bool prettyPrintJson = false, - int spacesPerIndent = 4) + int spacesPerIndent = 2) { var appliedTheme = theme ?? ThemePresets.Literate; var appliedFormatProvider = formatProvider ?? CultureInfo.InvariantCulture; diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs index 2f4c823..e291d3e 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs @@ -38,7 +38,7 @@ public class RichTextBoxSinkOptions /// Serilog output template that controls textual formatting of each log event. /// Optional culture-specific or custom formatting provider used when rendering scalar values; null for the invariant culture. /// When true, formats JSON values with indentation and line breaks for better readability. Defaults to false. - /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 2. public RichTextBoxSinkOptions( Theme theme, bool autoScroll = true, @@ -46,7 +46,7 @@ public RichTextBoxSinkOptions( string outputTemplate = DefaultOutputTemplate, IFormatProvider? formatProvider = null, bool prettyPrintJson = false, - int spacesPerIndent = 4) + int spacesPerIndent = 2) { AutoScroll = autoScroll; Theme = theme; @@ -83,7 +83,7 @@ public int SpacesPerIndent get => _spacesPerIndent; private set => _spacesPerIndent = value switch { - < 1 => 1, + < 0 => 0, > 16 => 16, _ => value }; From 03852f4028823e6e5715217b0aa2b1954fbbe392 Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 17:16:46 +0100 Subject: [PATCH 12/15] Update Serilog.Sinks.RichTextBox.WinForms.Colored.csproj --- README.md | 58 ++++++++++++------- ....Sinks.RichTextBox.WinForms.Colored.csproj | 2 +- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 448dad5..514dc14 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![Latest version](https://img.shields.io/nuget/v/Serilog.Sinks.RichTextBox.WinForms.Colored.svg)](https://www.nuget.org/packages/Serilog.Sinks.RichTextBox.WinForms.Colored) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -A [Serilog](https://github.com/serilog/serilog) sink that writes log events to a [WinForms RichTextBox](https://docs.microsoft.com/en-us/dotnet/desktop/winforms/controls/richtextbox-control-overview-windows-forms) with support for coloring and custom themes. +A [Serilog](https://github.com/serilog/serilog) sink that writes log events to +a [WinForms RichTextBox](https://docs.microsoft.com/en-us/dotnet/desktop/winforms/controls/richtextbox-control-overview-windows-forms) +with support for coloring and custom themes. ![Screenshot of Serilog.Sinks.RichTextBox.WinForms.Colored in action](https://raw.githubusercontent.com/vonhoff/Serilog.Sinks.RichTextBox.WinForms.Colored/master/screenshot.png) @@ -16,7 +18,8 @@ A [Serilog](https://github.com/serilog/serilog) sink that writes log events to a - High-performance asynchronous processing - Line limit to control memory usage - Support for pretty-printing of JSON objects -- WCAG compliant color schemes based on the [Serilog WPF RichTextBox](https://github.com/serilog-contrib/serilog-sinks-richtextbox) sink. +- WCAG compliant color schemes based on + the [Serilog WPF RichTextBox](https://github.com/serilog-contrib/serilog-sinks-richtextbox) sink. ## Getting Started @@ -51,36 +54,47 @@ Log.Logger = new LoggerConfiguration() Log.Information("Hello, world!"); ``` -See the [Extension Method](Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs) for more configuration options. +See the [Extension Method](Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs) +for more configuration options. -## Themes - -Available built-in themes: - -| Theme | Description | -|-----------------------------|------------------------------------------------------------------------------| -| `ThemePresets.Literate` | Styled to replicate the default theme of Serilog.Sinks.Console __(default)__ | -| `ThemePresets.Grayscale` | A theme using only shades of gray, white, and black | -| `ThemePresets.Colored` | A theme based on the original Serilog.Sinks.ColoredConsole sink | -| `ThemePresets.Luminous` | A light theme with high contrast for accessibility | +## Configuration Options -The themes based on the original sinks are slightly adjusted to be [WCAG compliant](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum), ensuring that the contrast ratio between text and background colors is at least 4.5:1. +| Option | Description | Default Value | +|-------------------|------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| +| `theme` | The color theme applied when rendering individual message tokens. | `ThemePresets.Literate` | +| `autoScroll` | When `true` (default) the target control scrolls automatically to the most recent log line. | `true` | +| `maxLogLines` | Maximum number of log events retained in the in-memory circular buffer and rendered in the control. | `256` | +| `outputTemplate` | Serilog output template that controls textual formatting of each log event. | `[${Timestamp:HH:mm:ss} ${Level:u3}] ${Message:lj}${NewLine}${Exception}` | +| `formatProvider` | Optional culture-specific or custom formatting provider used when rendering scalar values; `null` for the invariant culture. | `CultureInfo.InvariantCulture` | +| `prettyPrintJson` | When `true`, formats JSON values with indentation and line breaks for better readability. | `false` | +| `spacesPerIndent` | Number of spaces per indentation level when pretty printing JSON. | `2` | -You can create your own custom themes by creating a new instance of the [Theme](Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Themes/Theme.cs) class and passing it to the `RichTextBox` extension method. Look at the [existing themes](Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Themes/ThemePresets.cs) for examples. - -## FAQ +## Themes -### Why is the package name so long? +Available built-in themes: -Shorter alternatives were already reserved in NuGet. The descriptive name helps people find it more easily. +| Theme | Description | +|--------------------------|------------------------------------------------------------------------------| +| `ThemePresets.Literate` | Styled to replicate the default theme of Serilog.Sinks.Console __(default)__ | +| `ThemePresets.Grayscale` | A theme using only shades of gray, white, and black | +| `ThemePresets.Colored` | A theme based on the original Serilog.Sinks.ColoredConsole sink | +| `ThemePresets.Luminous` | A light theme with high contrast for accessibility | -### Why a WinForms RichTextBox and not WPF? +The themes based on the original sinks are slightly adjusted to +be [WCAG compliant](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum), ensuring that the contrast ratio +between text and background colors is at least 4.5:1. -This sink is designed for WinForms apps to avoid pulling in the WPF framework and its dependencies. +You can create your own custom themes by creating a new instance of +the [Theme](Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Themes/Theme.cs) class and passing it to +the `RichTextBox` extension method. Look at +the [existing themes](Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Themes/ThemePresets.cs) for +examples. ## Support the Project 💖 -This project has been maintained since 2022 and is still under active development. If you find it useful, please consider supporting it. Your support will help keep the project alive and allow me to dedicate more time to making improvements. You can support it through: +This project has been maintained since 2022 and is still under active development. If you find it useful, please +consider supporting it. Your support will help keep the project alive and allow me to dedicate more time to making +improvements. You can support it through: * [GitHub Sponsors](https://github.com/sponsors/vonhoff) * [Ko-fi](https://ko-fi.com/vonhoff) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj b/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj index 14c2c0c..03d1317 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj @@ -28,7 +28,7 @@ See repository for more information: https://github.com/vonhoff/Serilog.Sinks.RichTextBox.WinForms.Colored - + 7.0 windows en-US From 0317d7582335ad6f29fdad19b3d65586e7bdb9fc Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 18:36:04 +0100 Subject: [PATCH 13/15] Refactor renderer constructors to use options object Refactored all token renderer and TemplateRenderer constructors to accept a RichTextBoxSinkOptions object instead of individual parameters. Updated tests and extension methods accordingly. Standardized default JSON indentation to 2 spaces and removed unused parameters for consistency. --- README.md | 42 +++++++------------ .../PropertiesTokenRendererTests.cs | 16 ++++--- .../Integration/TokenRendererTests.cs | 19 ++++++--- .../RichTextBoxSinkTestBase.cs | 9 ++-- ...extBoxSinkLoggerConfigurationExtensions.cs | 6 +-- .../Formatting/DisplayValueFormatter.cs | 8 ++-- .../Formatting/JsonValueFormatter.cs | 2 +- .../Formatting/ValueFormatterState.cs | 2 +- .../Rendering/EventPropertyTokenRenderer.cs | 15 +++---- .../Rendering/MessageTemplateTokenRenderer.cs | 10 ++--- .../Rendering/PropertiesTokenRenderer.cs | 8 ++-- .../Rendering/TemplateRenderer.cs | 24 +++++------ .../Rendering/TimestampTokenRenderer.cs | 11 ++--- .../Sinks/RichTextBoxForms/RichTextBoxSink.cs | 2 +- 14 files changed, 83 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 514dc14..02dc429 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,7 @@ [![Latest version](https://img.shields.io/nuget/v/Serilog.Sinks.RichTextBox.WinForms.Colored.svg)](https://www.nuget.org/packages/Serilog.Sinks.RichTextBox.WinForms.Colored) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -A [Serilog](https://github.com/serilog/serilog) sink that writes log events to -a [WinForms RichTextBox](https://docs.microsoft.com/en-us/dotnet/desktop/winforms/controls/richtextbox-control-overview-windows-forms) -with support for coloring and custom themes. +A [Serilog](https://github.com/serilog/serilog) sink that writes log events to a [WinForms RichTextBox](https://docs.microsoft.com/en-us/dotnet/desktop/winforms/controls/richtextbox-control-overview-windows-forms) with support for coloring and custom themes. ![Screenshot of Serilog.Sinks.RichTextBox.WinForms.Colored in action](https://raw.githubusercontent.com/vonhoff/Serilog.Sinks.RichTextBox.WinForms.Colored/master/screenshot.png) @@ -18,8 +16,7 @@ with support for coloring and custom themes. - High-performance asynchronous processing - Line limit to control memory usage - Support for pretty-printing of JSON objects -- WCAG compliant color schemes based on - the [Serilog WPF RichTextBox](https://github.com/serilog-contrib/serilog-sinks-richtextbox) sink. +- WCAG compliant color schemes based on the [Serilog WPF RichTextBox](https://github.com/serilog-contrib/serilog-sinks-richtextbox) sink. ## Getting Started @@ -54,20 +51,19 @@ Log.Logger = new LoggerConfiguration() Log.Information("Hello, world!"); ``` -See the [Extension Method](Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs) -for more configuration options. +See the [Extension Method](Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs) for more configuration options. ## Configuration Options -| Option | Description | Default Value | -|-------------------|------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| -| `theme` | The color theme applied when rendering individual message tokens. | `ThemePresets.Literate` | -| `autoScroll` | When `true` (default) the target control scrolls automatically to the most recent log line. | `true` | -| `maxLogLines` | Maximum number of log events retained in the in-memory circular buffer and rendered in the control. | `256` | -| `outputTemplate` | Serilog output template that controls textual formatting of each log event. | `[${Timestamp:HH:mm:ss} ${Level:u3}] ${Message:lj}${NewLine}${Exception}` | -| `formatProvider` | Optional culture-specific or custom formatting provider used when rendering scalar values; `null` for the invariant culture. | `CultureInfo.InvariantCulture` | -| `prettyPrintJson` | When `true`, formats JSON values with indentation and line breaks for better readability. | `false` | -| `spacesPerIndent` | Number of spaces per indentation level when pretty printing JSON. | `2` | +| Option | Description | Default Value | +|-------------------|------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| `theme` | The color theme applied when rendering individual message tokens. | `ThemePresets.Literate` | +| `autoScroll` | When `true` (default) the target control scrolls automatically to the most recent log line. | `true` | +| `maxLogLines` | Maximum number of log events retained in the in-memory circular buffer and rendered in the control. | `256` | +| `outputTemplate` | Serilog output template that controls textual formatting of each log event. | `[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}` | +| `formatProvider` | Optional culture-specific or custom formatting provider used when rendering scalar values; `null` for the invariant culture. | `CultureInfo.InvariantCulture` | +| `prettyPrintJson` | When `true`, formats JSON values with indentation and line breaks for better readability. | `false` | +| `spacesPerIndent` | Number of spaces per indentation level when pretty printing JSON. | `2` | ## Themes @@ -80,21 +76,13 @@ Available built-in themes: | `ThemePresets.Colored` | A theme based on the original Serilog.Sinks.ColoredConsole sink | | `ThemePresets.Luminous` | A light theme with high contrast for accessibility | -The themes based on the original sinks are slightly adjusted to -be [WCAG compliant](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum), ensuring that the contrast ratio -between text and background colors is at least 4.5:1. +The themes based on the original sinks are slightly adjusted to be [WCAG compliant](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum), ensuring that the contrast ratio between text and background colors is at least 4.5:1. -You can create your own custom themes by creating a new instance of -the [Theme](Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Themes/Theme.cs) class and passing it to -the `RichTextBox` extension method. Look at -the [existing themes](Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Themes/ThemePresets.cs) for -examples. +You can create your own custom themes by creating a new instance of the [Theme](Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Themes/Theme.cs) class and passing it to the `RichTextBox` extension method. Look at the [existing themes](Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Themes/ThemePresets.cs) for examples. ## Support the Project 💖 -This project has been maintained since 2022 and is still under active development. If you find it useful, please -consider supporting it. Your support will help keep the project alive and allow me to dedicate more time to making -improvements. You can support it through: +This project has been maintained since 2022 and is still under active development. If you find it useful, please consider supporting it. Your support will help keep the project alive and allow me to dedicate more time to making improvements. You can support it through: * [GitHub Sponsors](https://github.com/sponsors/vonhoff) * [Ko-fi](https://ko-fi.com/vonhoff) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/PropertiesTokenRendererTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/PropertiesTokenRendererTests.cs index 1d3bee3..5b68e87 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/PropertiesTokenRendererTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/PropertiesTokenRendererTests.cs @@ -1,5 +1,6 @@ using Serilog.Events; using Serilog.Parsing; +using Serilog.Sinks.RichTextBoxForms; using Serilog.Sinks.RichTextBoxForms.Rendering; using Xunit; @@ -13,7 +14,8 @@ public void Render_WithAdditionalProperties_FormatsCorrectly() var template = _parser.Parse("Message: {Message}"); var outputTemplate = _parser.Parse("Message: {Message} {Properties}"); var token = outputTemplate.Tokens.OfType().Single(t => t.PropertyName == "Properties"); - var renderer = new PropertiesTokenRenderer(_defaultTheme, token, outputTemplate, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new PropertiesTokenRenderer(token, outputTemplate, options); var logEvent = new LogEvent( DateTimeOffset.Now, @@ -39,7 +41,8 @@ public void Render_WithJsonFormatting_FormatsCorrectly() var template = _parser.Parse("Message: {Message}"); var outputTemplate = _parser.Parse("Message: {Message} {Properties:j}"); var token = outputTemplate.Tokens.OfType().Single(t => t.PropertyName == "Properties"); - var renderer = new PropertiesTokenRenderer(_defaultTheme, token, outputTemplate, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new PropertiesTokenRenderer(token, outputTemplate, options); var logEvent = new LogEvent( DateTimeOffset.Now, @@ -67,7 +70,8 @@ public void Render_WithNestedProperties_FormatsCorrectly() var template = _parser.Parse("Message: {Message}"); var outputTemplate = _parser.Parse("Message: {Message} {Properties}"); var token = outputTemplate.Tokens.OfType().Single(t => t.PropertyName == "Properties"); - var renderer = new PropertiesTokenRenderer(_defaultTheme, token, outputTemplate, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new PropertiesTokenRenderer(token, outputTemplate, options); var nestedStructure = new StructureValue(new[] { @@ -98,7 +102,8 @@ public void Render_WithNoAdditionalProperties_FormatsCorrectly() var template = _parser.Parse("Message: {Message}"); var outputTemplate = _parser.Parse("Message: {Message} {Properties}"); var token = outputTemplate.Tokens.OfType().Single(t => t.PropertyName == "Properties"); - var renderer = new PropertiesTokenRenderer(_defaultTheme, token, outputTemplate, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new PropertiesTokenRenderer(token, outputTemplate, options); var logEvent = new LogEvent( DateTimeOffset.Now, @@ -122,7 +127,8 @@ public void Render_WithPropertiesInOutputTemplate_ExcludesThem() var template = _parser.Parse("Message: {Message}"); var outputTemplate = _parser.Parse("Message: {Message} {Properties} {Custom}"); var token = outputTemplate.Tokens.OfType().Single(t => t.PropertyName == "Properties"); - var renderer = new PropertiesTokenRenderer(_defaultTheme, token, outputTemplate, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new PropertiesTokenRenderer(token, outputTemplate, options); var logEvent = new LogEvent( DateTimeOffset.Now, diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs index 544d049..4b68373 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs @@ -1,5 +1,6 @@ using Serilog.Events; using Serilog.Parsing; +using Serilog.Sinks.RichTextBoxForms; using Serilog.Sinks.RichTextBoxForms.Rendering; using Xunit; @@ -60,7 +61,8 @@ public void EventPropertyTokenRenderer_RendersNonStringScalarValue() { var template = _parser.Parse("Number: {Number}"); var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "Number"); - var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new EventPropertyTokenRenderer(propertyToken, options); var logEvent = new LogEvent( DateTimeOffset.Now, @@ -80,7 +82,8 @@ public void EventPropertyTokenRenderer_RendersStructureValue() { var template = _parser.Parse("User: {User}"); var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "User"); - var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new EventPropertyTokenRenderer(propertyToken, options); var structureValue = new StructureValue(new[] { @@ -109,7 +112,8 @@ public void EventPropertyTokenRenderer_RendersSequenceValue() { var template = _parser.Parse("Items: {Items}"); var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "Items"); - var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new EventPropertyTokenRenderer(propertyToken, options); var sequenceValue = new SequenceValue(new[] { @@ -138,7 +142,8 @@ public void EventPropertyTokenRenderer_RendersDictionaryValue() { var template = _parser.Parse("Config: {Config}"); var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "Config"); - var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new EventPropertyTokenRenderer(propertyToken, options); var dict = new Dictionary { @@ -168,7 +173,8 @@ public void EventPropertyTokenRenderer_HandlesMissingProperty() { var template = _parser.Parse("Missing: {Missing}"); var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "Missing"); - var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new EventPropertyTokenRenderer(propertyToken, options); var logEvent = new LogEvent( DateTimeOffset.Now, @@ -188,7 +194,8 @@ public void EventPropertyTokenRenderer_RendersStringValue() { var template = _parser.Parse("Message: {Message}"); var propertyToken = template.Tokens.OfType().Single(t => t.PropertyName == "Message"); - var renderer = new EventPropertyTokenRenderer(_defaultTheme, propertyToken, null); + var options = new RichTextBoxSinkOptions(_defaultTheme, formatProvider: null); + var renderer = new EventPropertyTokenRenderer(propertyToken, options); var logEvent = new LogEvent( DateTimeOffset.Now, diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/RichTextBoxSinkTestBase.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/RichTextBoxSinkTestBase.cs index 234c65d..4855ec0 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/RichTextBoxSinkTestBase.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/RichTextBoxSinkTestBase.cs @@ -20,15 +20,16 @@ protected RichTextBoxSinkTestBase() { _richTextBox = new RichTextBox(); _defaultTheme = ThemePresets.Literate; - _renderer = new TemplateRenderer(_defaultTheme, "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}", null); _parser = new MessageTemplateParser(); var options = new RichTextBoxSinkOptions( theme: _defaultTheme, + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}", autoScroll: true, maxLogLines: 1000 ); + _renderer = new TemplateRenderer(options); _sink = new RichTextBoxSink(_richTextBox, options); // Wrap the RichTextBox in an IRtfCanvas adapter so that unit tests @@ -40,7 +41,8 @@ protected RichTextBoxSinkTestBase() protected string RenderAndGetText(LogEvent logEvent, string outputTemplate, IFormatProvider? formatProvider = null) { _richTextBox.Clear(); - var renderer = new TemplateRenderer(_defaultTheme, outputTemplate, formatProvider); + var options = new RichTextBoxSinkOptions(_defaultTheme, outputTemplate: outputTemplate, formatProvider: formatProvider); + var renderer = new TemplateRenderer(options); renderer.Render(logEvent, _canvas); return _richTextBox.Text.TrimEnd('\n', '\r'); } @@ -48,7 +50,8 @@ protected string RenderAndGetText(LogEvent logEvent, string outputTemplate, IFor protected string RenderAndGetText(LogEvent logEvent, string outputTemplate, RichTextBoxSinkOptions options) { _richTextBox.Clear(); - var renderer = new TemplateRenderer(options.Theme, outputTemplate, options.FormatProvider, options); + var rendererOptions = new RichTextBoxSinkOptions(options.Theme, options.AutoScroll, options.MaxLogLines, outputTemplate, options.FormatProvider, options.PrettyPrintJson, options.SpacesPerIndent); + var renderer = new TemplateRenderer(rendererOptions); renderer.Render(logEvent, _canvas); return _richTextBox.Text.TrimEnd('\n', '\r'); } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs index 7986dbe..6e094f1 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs @@ -65,7 +65,7 @@ public static LoggerConfiguration RichTextBox( var appliedTheme = theme ?? ThemePresets.Literate; var appliedFormatProvider = formatProvider ?? CultureInfo.InvariantCulture; var options = new RichTextBoxSinkOptions(appliedTheme, autoScroll, maxLogLines, outputTemplate, appliedFormatProvider, prettyPrintJson, spacesPerIndent); - var renderer = new TemplateRenderer(appliedTheme, outputTemplate, appliedFormatProvider, options); + var renderer = new TemplateRenderer(options); richTextBoxSink = new RichTextBoxSink(richTextBoxControl, options, renderer); return sinkConfiguration.Sink(richTextBoxSink, minimumLogEventLevel, levelSwitch); } @@ -83,7 +83,7 @@ public static LoggerConfiguration RichTextBox( /// Minimum log level for events to be written. /// Optional switch to change the minimum log level at runtime. /// If true, formats JSON values with indentation and line breaks. Defaults to false. - /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16. + /// Number of spaces per indentation level when pretty printing JSON. Defaults to 4. /// The logger configuration, for chaining. public static LoggerConfiguration RichTextBox( this LoggerSinkConfiguration sinkConfiguration, @@ -96,7 +96,7 @@ public static LoggerConfiguration RichTextBox( LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose, LoggingLevelSwitch? levelSwitch = null, bool prettyPrintJson = false, - int spacesPerIndent = 4) + int spacesPerIndent = 2) { return RichTextBox( sinkConfiguration, diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs index 3df3364..8d5809d 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs @@ -35,7 +35,7 @@ public class DisplayValueFormatter : ValueFormatter private readonly StringBuilder _literalBuilder = new(64); private JsonValueFormatter? _jsonValueFormatter; - public DisplayValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrintJson = false, int spacesPerIndent = 4, bool useSpacesForIndent = true) : base(theme, formatProvider) + public DisplayValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrintJson = false, int spacesPerIndent = 2) : base(theme, formatProvider) { _formatProvider = formatProvider; _prettyPrintJson = prettyPrintJson; @@ -104,7 +104,7 @@ protected override bool VisitDictionaryValue(ValueFormatterState state, Dictiona { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent, true); + _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent); _jsonValueFormatter.Format(dictionary, state.Canvas, state.Format, state.IsLiteral); return true; } @@ -140,7 +140,7 @@ protected override bool VisitSequenceValue(ValueFormatterState state, SequenceVa { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent, true); + _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent); _jsonValueFormatter.Format(sequence, state.Canvas, state.Format, state.IsLiteral); return true; } @@ -167,7 +167,7 @@ protected override bool VisitStructureValue(ValueFormatterState state, Structure { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent, true); + _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent); _jsonValueFormatter.Format(structure, state.Canvas, state.Format, state.IsLiteral); return true; } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs index aa84d7d..c8fb0cb 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs @@ -36,7 +36,7 @@ public class JsonValueFormatter : ValueFormatter private readonly StringBuilder _scalarBuilder = new(); private readonly StringBuilder _jsonStringBuilder = new(); - public JsonValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrint = false, int spacesPerIndent = 4, bool useSpacesForIndent = true) : base(theme, formatProvider) + public JsonValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrint = false, int spacesPerIndent = 2) : base(theme, formatProvider) { _formatProvider = formatProvider; _prettyPrint = prettyPrint; diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs index 10ae8d6..ad05c30 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs @@ -22,7 +22,7 @@ namespace Serilog.Sinks.RichTextBoxForms.Formatting { public readonly struct ValueFormatterState { - public ValueFormatterState(IRtfCanvas canvas, string format, bool isLiteral, int indentLevel = 0, int spacesPerIndent = 4, bool isTopLevel = true) + public ValueFormatterState(IRtfCanvas canvas, string format, bool isLiteral, int indentLevel = 0, int spacesPerIndent = 2, bool isTopLevel = true) { Canvas = canvas; Format = format; diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/EventPropertyTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/EventPropertyTokenRenderer.cs index 78093d2..e478147 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/EventPropertyTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/EventPropertyTokenRenderer.cs @@ -21,7 +21,6 @@ using Serilog.Sinks.RichTextBoxForms.Formatting; using Serilog.Sinks.RichTextBoxForms.Rtf; using Serilog.Sinks.RichTextBoxForms.Themes; -using System; using System.IO; using System.Text; @@ -29,15 +28,13 @@ namespace Serilog.Sinks.RichTextBoxForms.Rendering { public class EventPropertyTokenRenderer : ITokenRenderer { - private readonly IFormatProvider? _formatProvider; - private readonly Theme _theme; + private readonly RichTextBoxSinkOptions _options; private readonly PropertyToken _token; - public EventPropertyTokenRenderer(Theme theme, PropertyToken token, IFormatProvider? formatProvider) + public EventPropertyTokenRenderer(PropertyToken token, RichTextBoxSinkOptions options) { - _theme = theme; _token = token; - _formatProvider = formatProvider; + _options = options; } public void Render(LogEvent logEvent, IRtfCanvas canvas) @@ -50,7 +47,7 @@ public void Render(LogEvent logEvent, IRtfCanvas canvas) if (propertyValue is ScalarValue { Value: string literalString }) { var cased = TextFormatter.Format(literalString, _token.Format); - _theme.Render(canvas, StyleToken.SecondaryText, cased); + _options.Theme.Render(canvas, StyleToken.SecondaryText, cased); } else { @@ -58,10 +55,10 @@ public void Render(LogEvent logEvent, IRtfCanvas canvas) using (var writer = new StringWriter(sb)) { - propertyValue.Render(writer, _token.Format, _formatProvider); + propertyValue.Render(writer, _token.Format, _options.FormatProvider); } - _theme.Render(canvas, StyleToken.SecondaryText, sb.ToString()); + _options.Theme.Render(canvas, StyleToken.SecondaryText, sb.ToString()); } } } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs index 997090d..9d8703e 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs @@ -20,8 +20,6 @@ using Serilog.Parsing; using Serilog.Sinks.RichTextBoxForms.Formatting; using Serilog.Sinks.RichTextBoxForms.Rtf; -using Serilog.Sinks.RichTextBoxForms.Themes; -using System; namespace Serilog.Sinks.RichTextBoxForms.Rendering { @@ -29,16 +27,16 @@ public class MessageTemplateTokenRenderer : ITokenRenderer { private readonly MessageTemplateRenderer _renderer; - public MessageTemplateTokenRenderer(Theme theme, PropertyToken token, IFormatProvider? formatProvider, RichTextBoxSinkOptions? options = null) + public MessageTemplateTokenRenderer(PropertyToken token, RichTextBoxSinkOptions options) { var isLiteral = token.Format?.Contains("l") == true; var isJson = token.Format?.Contains("j") == true; ValueFormatter valueFormatter = isJson - ? new JsonValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.SpacesPerIndent ?? 4, true) - : new DisplayValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.SpacesPerIndent ?? 4, true); + ? new JsonValueFormatter(options.Theme, options.FormatProvider, options.PrettyPrintJson, options.SpacesPerIndent) + : new DisplayValueFormatter(options.Theme, options.FormatProvider, options.PrettyPrintJson, options.SpacesPerIndent); - _renderer = new MessageTemplateRenderer(theme, valueFormatter, isLiteral); + _renderer = new MessageTemplateRenderer(options.Theme, valueFormatter, isLiteral); } public void Render(LogEvent logEvent, IRtfCanvas canvas) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs index f7d4a6e..a53f150 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs @@ -20,8 +20,6 @@ using Serilog.Parsing; using Serilog.Sinks.RichTextBoxForms.Formatting; using Serilog.Sinks.RichTextBoxForms.Rtf; -using Serilog.Sinks.RichTextBoxForms.Themes; -using System; using System.Collections.Generic; using System.Linq; @@ -32,11 +30,11 @@ public class PropertiesTokenRenderer : ITokenRenderer private readonly ValueFormatter _valueFormatter; private readonly HashSet _outputTemplateProperties; - public PropertiesTokenRenderer(Theme theme, PropertyToken token, MessageTemplate outputTemplate, IFormatProvider? formatProvider, RichTextBoxSinkOptions? options = null) + public PropertiesTokenRenderer(PropertyToken token, MessageTemplate outputTemplate, RichTextBoxSinkOptions options) { _valueFormatter = token.Format?.Contains("j") == true - ? new JsonValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.SpacesPerIndent ?? 4, true) - : new DisplayValueFormatter(theme, formatProvider, options?.PrettyPrintJson ?? false, options?.SpacesPerIndent ?? 4, true); + ? new JsonValueFormatter(options.Theme, options.FormatProvider, options.PrettyPrintJson, options.SpacesPerIndent) + : new DisplayValueFormatter(options.Theme, options.FormatProvider, options.PrettyPrintJson, options.SpacesPerIndent); _outputTemplateProperties = new HashSet( outputTemplate.Tokens.OfType().Select(p => p.PropertyName)); diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs index b60a576..d09cce5 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TemplateRenderer.cs @@ -20,7 +20,6 @@ using Serilog.Formatting.Display; using Serilog.Parsing; using Serilog.Sinks.RichTextBoxForms.Rtf; -using Serilog.Sinks.RichTextBoxForms.Themes; using System; using System.Collections.Generic; @@ -28,23 +27,22 @@ namespace Serilog.Sinks.RichTextBoxForms.Rendering { public class TemplateRenderer : ITokenRenderer { - private const string OutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"; private readonly List _renderers; - public TemplateRenderer(Theme theme, string outputTemplate = OutputTemplate, IFormatProvider? formatProvider = null, RichTextBoxSinkOptions? options = null) + public TemplateRenderer(RichTextBoxSinkOptions options) { - if (string.IsNullOrEmpty(outputTemplate)) + if (string.IsNullOrEmpty(options.OutputTemplate)) { - throw new ArgumentNullException(nameof(outputTemplate)); + throw new ArgumentNullException(nameof(options.OutputTemplate)); } - var template = new MessageTemplateParser().Parse(outputTemplate); + var template = new MessageTemplateParser().Parse(options.OutputTemplate); _renderers = new List(); foreach (var token in template.Tokens) { if (token is TextToken textToken) { - _renderers.Add(new TextTokenRenderer(theme, textToken.Text)); + _renderers.Add(new TextTokenRenderer(options.Theme, textToken.Text)); continue; } @@ -54,7 +52,7 @@ public TemplateRenderer(Theme theme, string outputTemplate = OutputTemplate, IFo { case OutputProperties.LevelPropertyName: { - _renderers.Add(new LevelTokenRenderer(theme, propertyToken)); + _renderers.Add(new LevelTokenRenderer(options.Theme, propertyToken)); break; } @@ -66,31 +64,31 @@ public TemplateRenderer(Theme theme, string outputTemplate = OutputTemplate, IFo case OutputProperties.ExceptionPropertyName: { - _renderers.Add(new ExceptionTokenRenderer(theme)); + _renderers.Add(new ExceptionTokenRenderer(options.Theme)); break; } case OutputProperties.MessagePropertyName: { - _renderers.Add(new MessageTemplateTokenRenderer(theme, propertyToken, formatProvider, options)); + _renderers.Add(new MessageTemplateTokenRenderer(propertyToken, options)); break; } case OutputProperties.TimestampPropertyName: { - _renderers.Add(new TimestampTokenRenderer(theme, propertyToken, formatProvider)); + _renderers.Add(new TimestampTokenRenderer(propertyToken, options)); break; } case OutputProperties.PropertiesPropertyName: { - _renderers.Add(new PropertiesTokenRenderer(theme, propertyToken, template, formatProvider, options)); + _renderers.Add(new PropertiesTokenRenderer(propertyToken, template, options)); break; } default: { - _renderers.Add(new EventPropertyTokenRenderer(theme, propertyToken, formatProvider)); + _renderers.Add(new EventPropertyTokenRenderer(propertyToken, options)); break; } } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TimestampTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TimestampTokenRenderer.cs index 616e047..59d6a0e 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TimestampTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/TimestampTokenRenderer.cs @@ -20,26 +20,23 @@ using Serilog.Parsing; using Serilog.Sinks.RichTextBoxForms.Rtf; using Serilog.Sinks.RichTextBoxForms.Themes; -using System; namespace Serilog.Sinks.RichTextBoxForms.Rendering { public class TimestampTokenRenderer : ITokenRenderer { - private readonly IFormatProvider? _formatProvider; - private readonly Theme _theme; + private readonly RichTextBoxSinkOptions _options; private readonly PropertyToken _token; - public TimestampTokenRenderer(Theme theme, PropertyToken token, IFormatProvider? formatProvider) + public TimestampTokenRenderer(PropertyToken token, RichTextBoxSinkOptions options) { - _theme = theme; _token = token; - _formatProvider = formatProvider; + _options = options; } public void Render(LogEvent logEvent, IRtfCanvas canvas) { - _theme.Render(canvas, StyleToken.SecondaryText, logEvent.Timestamp.ToString(_token.Format, _formatProvider)); + _options.Theme.Render(canvas, StyleToken.SecondaryText, logEvent.Timestamp.ToString(_token.Format, _options.FormatProvider)); } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs index b9437ff..2bba4c1 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs @@ -45,7 +45,7 @@ public RichTextBoxSink(RichTextBox richTextBox, RichTextBoxSinkOptions options, { _options = options; _richTextBox = richTextBox; - _renderer = renderer ?? new TemplateRenderer(options.Theme, options.OutputTemplate, options.FormatProvider, options); + _renderer = renderer ?? new TemplateRenderer(options); _tokenSource = new CancellationTokenSource(); _buffer = new ConcurrentCircularBuffer(options.MaxLogLines); From b192fb06bd8488a86c03a7a3fc447d8e862aaa0a Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 18:52:13 +0100 Subject: [PATCH 14/15] Refactor value formatters to use options object Updated DisplayValueFormatter, JsonValueFormatter, and ValueFormatter to accept a RichTextBoxSinkOptions object instead of individual parameters. Updated all usages and internal references to use the options object, improving maintainability and consistency. --- .../Formatting/DisplayValueFormatter.cs | 58 ++++----- .../Formatting/JsonValueFormatter.cs | 122 +++++++++--------- .../Formatting/ValueFormatter.cs | 19 +-- .../Rendering/MessageTemplateTokenRenderer.cs | 4 +- .../Rendering/PropertiesTokenRenderer.cs | 4 +- 5 files changed, 94 insertions(+), 113 deletions(-) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs index 8d5809d..f723404 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/DisplayValueFormatter.cs @@ -28,18 +28,12 @@ namespace Serilog.Sinks.RichTextBoxForms.Formatting { public class DisplayValueFormatter : ValueFormatter { - private readonly IFormatProvider? _formatProvider; - private readonly bool _prettyPrintJson; - private readonly int _spacesPerIndent; private readonly StringBuilder _scalarBuilder = new(); private readonly StringBuilder _literalBuilder = new(64); private JsonValueFormatter? _jsonValueFormatter; - public DisplayValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrintJson = false, int spacesPerIndent = 2) : base(theme, formatProvider) + public DisplayValueFormatter(RichTextBoxSinkOptions options) : base(options) { - _formatProvider = formatProvider; - _prettyPrintJson = prettyPrintJson; - _spacesPerIndent = spacesPerIndent; } private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas, string? format, bool isLiteral) @@ -49,7 +43,7 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas, string? f switch (value) { case null: - Theme.Render(canvas, StyleToken.Null, "null"); + Options.Theme.Render(canvas, StyleToken.Null, "null"); return; case string text: @@ -59,15 +53,15 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas, string? f case byte[] bytes: _literalBuilder.Clear(); _literalBuilder.Append('"').Append(Convert.ToBase64String(bytes)).Append('"'); - Theme.Render(canvas, StyleToken.String, _literalBuilder.ToString()); + Options.Theme.Render(canvas, StyleToken.String, _literalBuilder.ToString()); return; case bool b: - Theme.Render(canvas, StyleToken.Boolean, b.ToString()); + Options.Theme.Render(canvas, StyleToken.Boolean, b.ToString()); return; case Uri uri: - Theme.Render(canvas, StyleToken.Scalar, uri.ToString()); + Options.Theme.Render(canvas, StyleToken.Scalar, uri.ToString()); return; case IFormattable formattable: @@ -79,10 +73,10 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas, string? f using (var writer = new StringWriter(_scalarBuilder)) { - scalar.Render(writer, null, _formatProvider); + scalar.Render(writer, null, Options.FormatProvider); } - Theme.Render(canvas, StyleToken.Scalar, _scalarBuilder.ToString()); + Options.Theme.Render(canvas, StyleToken.Scalar, _scalarBuilder.ToString()); } private void RenderString(string text, IRtfCanvas canvas, string? format, bool isLiteral) @@ -90,13 +84,13 @@ private void RenderString(string text, IRtfCanvas canvas, string? format, bool i var effectivelyLiteral = isLiteral || format != null && format.Contains("l"); if (effectivelyLiteral) { - Theme.Render(canvas, StyleToken.String, text); + Options.Theme.Render(canvas, StyleToken.String, text); } else { _literalBuilder.Clear(); _literalBuilder.Append('"').Append(text.Replace("\"", "\\\"")).Append('"'); - Theme.Render(canvas, StyleToken.String, _literalBuilder.ToString()); + Options.Theme.Render(canvas, StyleToken.String, _literalBuilder.ToString()); } } @@ -104,29 +98,29 @@ protected override bool VisitDictionaryValue(ValueFormatterState state, Dictiona { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent); + _jsonValueFormatter ??= new JsonValueFormatter(Options); _jsonValueFormatter.Format(dictionary, state.Canvas, state.Format, state.IsLiteral); return true; } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); var delimiter = string.Empty; foreach (var (scalarValue, propertyValue) in dictionary.Elements) { if (!string.IsNullOrEmpty(delimiter)) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); } delimiter = ", "; - Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); Visit(state.Next(), scalarValue); - Theme.Render(state.Canvas, StyleToken.TertiaryText, "]="); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "]="); Visit(state.Next(), propertyValue); } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); return true; } @@ -140,26 +134,26 @@ protected override bool VisitSequenceValue(ValueFormatterState state, SequenceVa { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent); + _jsonValueFormatter ??= new JsonValueFormatter(Options); _jsonValueFormatter.Format(sequence, state.Canvas, state.Format, state.IsLiteral); return true; } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); var delimiter = string.Empty; foreach (var propertyValue in sequence.Elements) { if (!string.IsNullOrEmpty(delimiter)) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); } delimiter = ", "; Visit(state.Next(), propertyValue); } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "]"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "]"); return true; } @@ -167,33 +161,33 @@ protected override bool VisitStructureValue(ValueFormatterState state, Structure { if (state.Format.Contains("j")) { - _jsonValueFormatter ??= new JsonValueFormatter(Theme, _formatProvider, _prettyPrintJson, _spacesPerIndent); + _jsonValueFormatter ??= new JsonValueFormatter(Options); _jsonValueFormatter.Format(structure, state.Canvas, state.Format, state.IsLiteral); return true; } if (structure.TypeTag != null) { - Theme.Render(state.Canvas, StyleToken.Name, structure.TypeTag + " "); + Options.Theme.Render(state.Canvas, StyleToken.Name, structure.TypeTag + " "); } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); var delimiter = string.Empty; foreach (var eventProperty in structure.Properties) { if (!string.IsNullOrEmpty(delimiter)) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); } delimiter = ", "; - Theme.Render(state.Canvas, StyleToken.Name, eventProperty.Name); - Theme.Render(state.Canvas, StyleToken.TertiaryText, "="); + Options.Theme.Render(state.Canvas, StyleToken.Name, eventProperty.Name); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "="); Visit(state.Next(), eventProperty.Value); } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); return true; } } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs index c8fb0cb..c3da3a6 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs @@ -29,23 +29,17 @@ namespace Serilog.Sinks.RichTextBoxForms.Formatting { public class JsonValueFormatter : ValueFormatter { - private readonly IFormatProvider? _formatProvider; - private readonly bool _prettyPrint; - private readonly int _spacesPerIndent; private readonly StringBuilder _literalBuilder = new(64); private readonly StringBuilder _scalarBuilder = new(); private readonly StringBuilder _jsonStringBuilder = new(); - public JsonValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrint = false, int spacesPerIndent = 2) : base(theme, formatProvider) + public JsonValueFormatter(RichTextBoxSinkOptions options) : base(options) { - _formatProvider = formatProvider; - _prettyPrint = prettyPrint; - _spacesPerIndent = spacesPerIndent; } protected override ValueFormatterState CreateInitialState(IRtfCanvas canvas, string format, bool isLiteral) { - return new ValueFormatterState(canvas, format, isLiteral, 0, _spacesPerIndent, true); + return new ValueFormatterState(canvas, format, isLiteral, 0, Options.SpacesPerIndent, true); } protected override bool VisitScalarValue(ValueFormatterState state, ScalarValue scalar) @@ -56,16 +50,16 @@ protected override bool VisitScalarValue(ValueFormatterState state, ScalarValue protected override bool VisitSequenceValue(ValueFormatterState state, SequenceValue sequence) { - if (_prettyPrint) + if (Options.PrettyPrintJson) { var indentState = state.ToIndentUp(); if (sequence.Elements.Count > 0) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "[\n" + indentState.GetIndentation()); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "[\n" + indentState.GetIndentation()); } else { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); } var delimiter = string.Empty; @@ -73,7 +67,7 @@ protected override bool VisitSequenceValue(ValueFormatterState state, SequenceVa { if (!string.IsNullOrEmpty(delimiter)) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); } delimiter = ",\n" + indentState.GetIndentation(); @@ -82,30 +76,30 @@ protected override bool VisitSequenceValue(ValueFormatterState state, SequenceVa if (sequence.Elements.Count > 0) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "\n" + state.GetIndentation() + "]"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "\n" + state.GetIndentation() + "]"); } else { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "]"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "]"); } } else { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "["); var delimiter = string.Empty; foreach (var propertyValue in sequence.Elements) { if (!string.IsNullOrEmpty(delimiter)) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); } delimiter = ", "; Visit(state, propertyValue); } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "]"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "]"); } return true; @@ -113,16 +107,16 @@ protected override bool VisitSequenceValue(ValueFormatterState state, SequenceVa protected override bool VisitStructureValue(ValueFormatterState state, StructureValue structure) { - if (_prettyPrint) + if (Options.PrettyPrintJson) { var indentState = state.ToIndentUp(); if (structure.Properties.Count > 0 || structure.TypeTag != null) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "{\n" + indentState.GetIndentation()); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "{\n" + indentState.GetIndentation()); } else { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); } var delimiter = string.Empty; @@ -130,59 +124,59 @@ protected override bool VisitStructureValue(ValueFormatterState state, Structure { if (!string.IsNullOrEmpty(delimiter)) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); } delimiter = ",\n" + indentState.GetIndentation(); - Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString(eventProperty.Name)); - Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + Options.Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString(eventProperty.Name)); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); Visit(indentState.Next(), eventProperty.Value); } if (structure.TypeTag != null) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); - Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString("$type")); - Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); - Theme.Render(state.Canvas, StyleToken.String, GetQuotedJsonString(structure.TypeTag)); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString("$type")); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + Options.Theme.Render(state.Canvas, StyleToken.String, GetQuotedJsonString(structure.TypeTag)); } if (structure.Properties.Count > 0 || structure.TypeTag != null) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "\n" + state.GetIndentation() + "}"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "\n" + state.GetIndentation() + "}"); } else { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); } } else { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); var delimiter = string.Empty; foreach (var eventProperty in structure.Properties) { if (!string.IsNullOrEmpty(delimiter)) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); } delimiter = ", "; - Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString(eventProperty.Name)); - Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + Options.Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString(eventProperty.Name)); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); Visit(state.Next(), eventProperty.Value); } if (structure.TypeTag != null) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); - Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString("$type")); - Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); - Theme.Render(state.Canvas, StyleToken.String, GetQuotedJsonString(structure.TypeTag)); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.Name, GetQuotedJsonString("$type")); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + Options.Theme.Render(state.Canvas, StyleToken.String, GetQuotedJsonString(structure.TypeTag)); } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); } return true; @@ -190,16 +184,16 @@ protected override bool VisitStructureValue(ValueFormatterState state, Structure protected override bool VisitDictionaryValue(ValueFormatterState state, DictionaryValue dictionary) { - if (_prettyPrint) + if (Options.PrettyPrintJson) { var indentState = state.ToIndentUp(); if (dictionary.Elements.Count > 0) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "{\n" + indentState.GetIndentation()); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "{\n" + indentState.GetIndentation()); } else { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); } var delimiter = string.Empty; @@ -207,7 +201,7 @@ protected override bool VisitDictionaryValue(ValueFormatterState state, Dictiona { if (!string.IsNullOrEmpty(delimiter)) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); } delimiter = ",\n" + indentState.GetIndentation(); @@ -218,31 +212,31 @@ protected override bool VisitDictionaryValue(ValueFormatterState state, Dictiona _ => StyleToken.Scalar }; - Theme.Render(state.Canvas, style, GetQuotedJsonString(scalar.Value?.ToString() ?? "null")); - Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + Options.Theme.Render(state.Canvas, style, GetQuotedJsonString(scalar.Value?.ToString() ?? "null")); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); Visit(indentState.Next(), propertyValue); } if (dictionary.Elements.Count > 0) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "\n" + state.GetIndentation() + "}"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "\n" + state.GetIndentation() + "}"); } else { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); } } else { - Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "{"); var delimiter = string.Empty; foreach (var (scalar, propertyValue) in dictionary.Elements) { if (!string.IsNullOrEmpty(delimiter)) { - Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, delimiter); } delimiter = ", "; @@ -253,13 +247,13 @@ protected override bool VisitDictionaryValue(ValueFormatterState state, Dictiona _ => StyleToken.Scalar }; - Theme.Render(state.Canvas, style, GetQuotedJsonString(scalar.Value?.ToString() ?? "null")); - Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); + Options.Theme.Render(state.Canvas, style, GetQuotedJsonString(scalar.Value?.ToString() ?? "null")); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, ": "); Visit(state.Next(), propertyValue); } - Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); + Options.Theme.Render(state.Canvas, StyleToken.TertiaryText, "}"); } return true; @@ -272,40 +266,40 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas) switch (value) { case null: - Theme.Render(canvas, StyleToken.Null, "null"); + Options.Theme.Render(canvas, StyleToken.Null, "null"); return; case string str: - Theme.Render(canvas, StyleToken.String, GetQuotedJsonString(str)); + Options.Theme.Render(canvas, StyleToken.String, GetQuotedJsonString(str)); return; case byte[] bytes: - Theme.Render(canvas, StyleToken.String, GetQuotedJsonString(Convert.ToBase64String(bytes))); + Options.Theme.Render(canvas, StyleToken.String, GetQuotedJsonString(Convert.ToBase64String(bytes))); return; case bool b: - Theme.Render(canvas, StyleToken.Boolean, b ? "true" : "false"); + Options.Theme.Render(canvas, StyleToken.Boolean, b ? "true" : "false"); return; case double d: if (double.IsNaN(d) || double.IsInfinity(d)) { - Theme.Render(canvas, StyleToken.Number, GetQuotedJsonString(d.ToString(CultureInfo.InvariantCulture))); + Options.Theme.Render(canvas, StyleToken.Number, GetQuotedJsonString(d.ToString(CultureInfo.InvariantCulture))); } else { - Theme.Render(canvas, StyleToken.Number, d.ToString("R", CultureInfo.InvariantCulture)); + Options.Theme.Render(canvas, StyleToken.Number, d.ToString("R", CultureInfo.InvariantCulture)); } return; case float f: if (float.IsNaN(f) || float.IsInfinity(f)) { - Theme.Render(canvas, StyleToken.Number, GetQuotedJsonString(f.ToString(CultureInfo.InvariantCulture))); + Options.Theme.Render(canvas, StyleToken.Number, GetQuotedJsonString(f.ToString(CultureInfo.InvariantCulture))); } else { - Theme.Render(canvas, StyleToken.Number, f.ToString("R", CultureInfo.InvariantCulture)); + Options.Theme.Render(canvas, StyleToken.Number, f.ToString("R", CultureInfo.InvariantCulture)); } return; @@ -331,18 +325,18 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas) break; default: - scalar.Render(writer, null, _formatProvider); + scalar.Render(writer, null, Options.FormatProvider); break; } } - Theme.Render(canvas, StyleToken.Scalar, GetQuotedJsonString(_literalBuilder.ToString())); + Options.Theme.Render(canvas, StyleToken.Scalar, GetQuotedJsonString(_literalBuilder.ToString())); return; default: if (value is ValueType and (int or uint or long or ulong or decimal or byte or sbyte or short or ushort)) { - Theme.Render(canvas, StyleToken.Number, ((IFormattable)value).ToString(null, CultureInfo.InvariantCulture)); + Options.Theme.Render(canvas, StyleToken.Number, ((IFormattable)value).ToString(null, CultureInfo.InvariantCulture)); return; } @@ -356,10 +350,10 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas) using (var writer = new StringWriter(_scalarBuilder)) { - scalar.Render(writer, null, _formatProvider); + scalar.Render(writer, null, Options.FormatProvider); } - Theme.Render(canvas, StyleToken.Scalar, GetQuotedJsonString(_scalarBuilder.ToString())); + Options.Theme.Render(canvas, StyleToken.Scalar, GetQuotedJsonString(_scalarBuilder.ToString())); return; } } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatter.cs index 6f76019..f893bee 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatter.cs @@ -28,15 +28,13 @@ namespace Serilog.Sinks.RichTextBoxForms.Formatting public abstract class ValueFormatter : LogEventPropertyValueVisitor { private readonly StringBuilder _formatBuilder = new(); - private readonly IFormatProvider? _formatProvider; - protected ValueFormatter(Theme theme, IFormatProvider? formatProvider = null) + protected ValueFormatter(RichTextBoxSinkOptions options) { - Theme = theme; - _formatProvider = formatProvider; + Options = options; } - protected Theme Theme { get; } + protected RichTextBoxSinkOptions Options { get; } public void Format(LogEventPropertyValue value, IRtfCanvas canvas, string format, bool isLiteral) { @@ -48,11 +46,6 @@ protected virtual ValueFormatterState CreateInitialState(IRtfCanvas canvas, stri return new ValueFormatterState(canvas, format, isLiteral); } - /// - /// Determines the appropriate StyleToken for a given type. - /// - /// The type to analyze - /// StyleToken.Number for numeric types, StyleToken.Scalar for others protected static StyleToken GetStyleTokenForType(Type type) { return type == typeof(float) || type == typeof(double) || type == typeof(decimal) || @@ -105,15 +98,15 @@ protected void RenderFormattable(IRtfCanvas canvas, IFormattable formattable, st string renderedValue; try { - renderedValue = formattable.ToString(effectiveFormat, _formatProvider); + renderedValue = formattable.ToString(effectiveFormat, Options.FormatProvider); } catch (FormatException) { // Fall back to the default formatting if the specified format is not supported by the value. - renderedValue = formattable.ToString(null, _formatProvider); + renderedValue = formattable.ToString(null, Options.FormatProvider); } - Theme.Render(canvas, token, renderedValue); + Options.Theme.Render(canvas, token, renderedValue); } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs index 9d8703e..e6f8474 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/MessageTemplateTokenRenderer.cs @@ -33,8 +33,8 @@ public MessageTemplateTokenRenderer(PropertyToken token, RichTextBoxSinkOptions var isJson = token.Format?.Contains("j") == true; ValueFormatter valueFormatter = isJson - ? new JsonValueFormatter(options.Theme, options.FormatProvider, options.PrettyPrintJson, options.SpacesPerIndent) - : new DisplayValueFormatter(options.Theme, options.FormatProvider, options.PrettyPrintJson, options.SpacesPerIndent); + ? new JsonValueFormatter(options) + : new DisplayValueFormatter(options); _renderer = new MessageTemplateRenderer(options.Theme, valueFormatter, isLiteral); } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs index a53f150..a5506e7 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/PropertiesTokenRenderer.cs @@ -33,8 +33,8 @@ public class PropertiesTokenRenderer : ITokenRenderer public PropertiesTokenRenderer(PropertyToken token, MessageTemplate outputTemplate, RichTextBoxSinkOptions options) { _valueFormatter = token.Format?.Contains("j") == true - ? new JsonValueFormatter(options.Theme, options.FormatProvider, options.PrettyPrintJson, options.SpacesPerIndent) - : new DisplayValueFormatter(options.Theme, options.FormatProvider, options.PrettyPrintJson, options.SpacesPerIndent); + ? new JsonValueFormatter(options) + : new DisplayValueFormatter(options); _outputTemplateProperties = new HashSet( outputTemplate.Tokens.OfType().Select(p => p.PropertyName)); From c13562e8e089ae9accff6aa5b283077cc79c9013 Mon Sep 17 00:00:00 2001 From: Simon Vonhoff Date: Sun, 9 Nov 2025 21:17:41 +0100 Subject: [PATCH 15/15] Refactor JSON formatting and rendering internals Removed obsolete parameters from StructureValue and ValueFormatterState constructors to align with updated Serilog API. --- .../Integration/JsonFormattingTests.cs | 2 +- .../RichTextBoxForms/Formatting/JsonValueFormatter.cs | 4 ++-- .../Rendering/EventPropertyTokenRenderer.cs | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs index 42e8891..bbb7f0c 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs @@ -185,7 +185,7 @@ public void PrettyPrintJson_EmptyCollectionsFormatCorrectly() { var emptyArray = new LogEventProperty("EmptyArray", new SequenceValue(Array.Empty())); var emptyDict = new LogEventProperty("EmptyDict", new DictionaryValue(new Dictionary())); - var emptyObject = new LogEventProperty("EmptyObject", new StructureValue(Array.Empty(), null)); + var emptyObject = new LogEventProperty("EmptyObject", new StructureValue(Array.Empty())); var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("Array: {EmptyArray:j}, Dict: {EmptyDict:j}, Object: {EmptyObject:j}"), new[] { emptyArray, emptyDict, emptyObject }); var options = new RichTextBoxSinkOptions( diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs index c3da3a6..5654420 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs @@ -29,7 +29,7 @@ namespace Serilog.Sinks.RichTextBoxForms.Formatting { public class JsonValueFormatter : ValueFormatter { - private readonly StringBuilder _literalBuilder = new(64); + private readonly StringBuilder _literalBuilder = new(); private readonly StringBuilder _scalarBuilder = new(); private readonly StringBuilder _jsonStringBuilder = new(); @@ -39,7 +39,7 @@ public JsonValueFormatter(RichTextBoxSinkOptions options) : base(options) protected override ValueFormatterState CreateInitialState(IRtfCanvas canvas, string format, bool isLiteral) { - return new ValueFormatterState(canvas, format, isLiteral, 0, Options.SpacesPerIndent, true); + return new ValueFormatterState(canvas, format, isLiteral, 0, Options.SpacesPerIndent); } protected override bool VisitScalarValue(ValueFormatterState state, ScalarValue scalar) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/EventPropertyTokenRenderer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/EventPropertyTokenRenderer.cs index e478147..d18b2da 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/EventPropertyTokenRenderer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Rendering/EventPropertyTokenRenderer.cs @@ -30,6 +30,7 @@ public class EventPropertyTokenRenderer : ITokenRenderer { private readonly RichTextBoxSinkOptions _options; private readonly PropertyToken _token; + private readonly StringBuilder _stringBuilder = new(); public EventPropertyTokenRenderer(PropertyToken token, RichTextBoxSinkOptions options) { @@ -51,14 +52,14 @@ public void Render(LogEvent logEvent, IRtfCanvas canvas) } else { - var sb = new StringBuilder(); + _stringBuilder.Clear(); - using (var writer = new StringWriter(sb)) + using (var writer = new StringWriter(_stringBuilder)) { propertyValue.Render(writer, _token.Format, _options.FormatProvider); } - _options.Theme.Render(canvas, StyleToken.SecondaryText, sb.ToString()); + _options.Theme.Render(canvas, StyleToken.SecondaryText, _stringBuilder.ToString()); } } }