Skip to content

Commit 84cf276

Browse files
committed
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.
1 parent 9b0c253 commit 84cf276

File tree

8 files changed

+108
-81
lines changed

8 files changed

+108
-81
lines changed

.github/workflows/build.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,42 @@ on:
99
jobs:
1010
build-and-test:
1111
runs-on: windows-latest
12-
12+
1313
steps:
1414
- uses: actions/checkout@v4
15-
15+
1616
- name: Setup .NET 8.0
1717
uses: actions/setup-dotnet@v5
1818
with:
1919
dotnet-version: 8.0.x
20-
20+
2121
- name: Setup MSBuild
2222
uses: microsoft/setup-msbuild@v2
23-
23+
2424
- name: Restore dependencies
2525
run: dotnet restore
26-
26+
2727
- name: Build
2828
run: dotnet build --no-restore --configuration Release --framework net8.0-windows
29-
29+
3030
- name: Test with Coverage
3131
run: dotnet test --no-build --configuration Release --framework net8.0-windows --collect:"XPlat Code Coverage" --results-directory ./coverage
32+
3233
- name: Generate coverage report
3334
uses: danielpalme/ReportGenerator-GitHub-Action@v5
3435
with:
3536
reports: './coverage/**/coverage.cobertura.xml'
3637
targetdir: './coverage/report'
3738
reporttypes: 'Html;HtmlSummary'
3839
title: 'Code Coverage Report'
40+
3941
- name: Upload coverage report
4042
uses: actions/upload-artifact@v4
4143
with:
4244
name: coverage-report
4345
path: './coverage/report'
4446
continue-on-error: true
47+
4548
- name: Check coverage threshold
4649
run: |
4750
$coverage = Select-Xml -Path "./coverage/**/coverage.cobertura.xml" -XPath "//coverage/@line-rate" | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value

Demo/Form1.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,11 @@ private void btnPrettyPrint_Click(object sender, EventArgs e)
357357
{
358358
_prettyPrintJson = !_prettyPrintJson;
359359
btnPrettyPrint.Text = _prettyPrintJson ? "Disable Pretty Print" : "Enable Pretty Print";
360-
360+
361361
// Recreate the sink and logger with new pretty print setting
362362
CloseAndFlush();
363363
Initialize();
364-
364+
365365
Log.Information("Pretty print JSON: {PrettyPrint}", _prettyPrintJson);
366366
}
367367

Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public void PrettyPrintJson_UsesTabsWhenConfigured()
220220
useSpacesForIndent: false);
221221

222222
var result = RenderAndGetText(logEvent, "{Message:l}", options);
223-
var expected = "{\n\t\"Id\": 123,\n\t\"$type\": \"MyObj\"\n}";
223+
var expected = "{\n\t\t\t\t\"Id\": 123,\n\t\t\t\t\"$type\": \"MyObj\"\n}";
224224
Assert.Equal(expected, result);
225225
}
226226

@@ -244,6 +244,26 @@ public void PrettyPrintJson_RespectsIndentSize()
244244
Assert.Equal(expected, result);
245245
}
246246

247+
[Fact]
248+
public void PrettyPrintJson_RespectsIndentSizeWithTabs()
249+
{
250+
var prop = new LogEventProperty("Test", new StructureValue(new[]
251+
{
252+
new LogEventProperty("Id", new ScalarValue(123))
253+
}, "MyObj"));
254+
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Test:j}"), new[] { prop });
255+
256+
var options = new RichTextBoxSinkOptions(
257+
theme: _defaultTheme,
258+
prettyPrintJson: true,
259+
indentSize: 2,
260+
useSpacesForIndent: false);
261+
262+
var result = RenderAndGetText(logEvent, "{Message:l}", options);
263+
var expected = "{\n\t\t\"Id\": 123,\n\t\t\"$type\": \"MyObj\"\n}";
264+
Assert.Equal(expected, result);
265+
}
266+
247267
[Fact]
248268
public void CompactJson_StillWorksByDefault()
249269
{

Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/TokenRendererTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ public class TokenRendererTests : RichTextBoxSinkTestBase
1010
[Fact]
1111
public void ExceptionTokenRenderer_RendersExceptionWithStackFrames()
1212
{
13-
Exception? exception = null;
13+
Exception? exception;
1414
try
1515
{
1616
throw new InvalidOperationException("Test exception");
1717
}
18-
catch (Exception ex)
18+
catch (InvalidOperationException ex)
1919
{
2020
exception = ex;
2121
}

Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public static class RichTextBoxSinkLoggerConfigurationExtensions
4646
/// <param name="minimumLogEventLevel">Minimum log level for events to be written.</param>
4747
/// <param name="levelSwitch">Optional switch to change the minimum log level at runtime.</param>
4848
/// <param name="prettyPrintJson">If <c>true</c>, formats JSON values with indentation and line breaks. Defaults to <c>false</c>.</param>
49-
/// <param name="indentSize">Number of spaces per indentation level when pretty printing JSON. Defaults to 4.</param>
49+
/// <param name="indentSize">Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16.</param>
5050
/// <param name="useSpacesForIndent">If <c>true</c> (default), uses spaces for indentation; otherwise uses tabs.</param>
5151
/// <returns>The logger configuration, for chaining.</returns>
5252
public static LoggerConfiguration RichTextBox(
@@ -85,7 +85,7 @@ public static LoggerConfiguration RichTextBox(
8585
/// <param name="minimumLogEventLevel">Minimum log level for events to be written.</param>
8686
/// <param name="levelSwitch">Optional switch to change the minimum log level at runtime.</param>
8787
/// <param name="prettyPrintJson">If <c>true</c>, formats JSON values with indentation and line breaks. Defaults to <c>false</c>.</param>
88-
/// <param name="indentSize">Number of spaces per indentation level when pretty printing JSON. Defaults to 4.</param>
88+
/// <param name="indentSize">Number of spaces per indentation level when pretty printing JSON. Defaults to 4. Must be between 1 and 16.</param>
8989
/// <param name="useSpacesForIndent">If <c>true</c> (default), uses spaces for indentation; otherwise uses tabs.</param>
9090
/// <returns>The logger configuration, for chaining.</returns>
9191
public static LoggerConfiguration RichTextBox(

Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/JsonValueFormatter.cs

Lines changed: 57 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class JsonValueFormatter : ValueFormatter
3535
private readonly bool _useSpacesForIndent;
3636
private readonly StringBuilder _literalBuilder = new(64);
3737
private readonly StringBuilder _scalarBuilder = new();
38+
private readonly StringBuilder _jsonStringBuilder = new();
3839

3940
public JsonValueFormatter(Theme theme, IFormatProvider? formatProvider, bool prettyPrint = false, int indentSize = 4, bool useSpacesForIndent = true) : base(theme, formatProvider)
4041
{
@@ -365,82 +366,75 @@ private void FormatLiteralValue(ScalarValue scalar, IRtfCanvas canvas)
365366
}
366367
}
367368

368-
/// <summary>
369-
/// Write a valid JSON string literal, escaping as necessary.
370-
/// Optimized version that avoids unnecessary string operations.
371-
/// </summary>
372-
/// <param name="str">The string value to write.</param>
373-
public static string GetQuotedJsonString(string str)
369+
private string GetQuotedJsonString(string str)
374370
{
375-
using (var output = new StringWriter())
376-
{
377-
output.Write('\"');
371+
_jsonStringBuilder.Clear();
372+
_jsonStringBuilder.Append('\"');
378373

379-
var cleanSegmentStart = 0;
380-
var anyEscaped = false;
374+
var cleanSegmentStart = 0;
375+
var anyEscaped = false;
381376

382-
for (var i = 0; i < str.Length; ++i)
377+
for (var i = 0; i < str.Length; ++i)
378+
{
379+
var c = str[i];
380+
if (c < (char)32 || c == '\\' || c == '"')
383381
{
384-
var c = str[i];
385-
if (c < (char)32 || c == '\\' || c == '"')
386-
{
387-
anyEscaped = true;
388-
389-
if (i > cleanSegmentStart)
390-
{
391-
output.Write(str.Substring(cleanSegmentStart, i - cleanSegmentStart));
392-
}
393-
cleanSegmentStart = i + 1;
394-
395-
switch (c)
396-
{
397-
case '"':
398-
output.Write("\\\"");
399-
break;
400-
401-
case '\\':
402-
output.Write("\\\\");
403-
break;
404-
405-
case '\n':
406-
output.Write("\\n");
407-
break;
408-
409-
case '\r':
410-
output.Write("\\r");
411-
break;
412-
413-
case '\f':
414-
output.Write("\\f");
415-
break;
382+
anyEscaped = true;
416383

417-
case '\t':
418-
output.Write("\\t");
419-
break;
420-
421-
default:
422-
output.Write("\\u");
423-
output.Write(((int)c).ToString("X4"));
424-
break;
425-
}
384+
if (i > cleanSegmentStart)
385+
{
386+
_jsonStringBuilder.Append(str, cleanSegmentStart, i - cleanSegmentStart);
426387
}
427-
}
388+
cleanSegmentStart = i + 1;
428389

429-
if (anyEscaped)
430-
{
431-
if (cleanSegmentStart < str.Length)
390+
switch (c)
432391
{
433-
output.Write(str.Substring(cleanSegmentStart));
392+
case '"':
393+
_jsonStringBuilder.Append("\\\"");
394+
break;
395+
396+
case '\\':
397+
_jsonStringBuilder.Append("\\\\");
398+
break;
399+
400+
case '\n':
401+
_jsonStringBuilder.Append("\\n");
402+
break;
403+
404+
case '\r':
405+
_jsonStringBuilder.Append("\\r");
406+
break;
407+
408+
case '\f':
409+
_jsonStringBuilder.Append("\\f");
410+
break;
411+
412+
case '\t':
413+
_jsonStringBuilder.Append("\\t");
414+
break;
415+
416+
default:
417+
_jsonStringBuilder.Append("\\u");
418+
_jsonStringBuilder.Append(((int)c).ToString("X4"));
419+
break;
434420
}
435421
}
436-
else
422+
}
423+
424+
if (anyEscaped)
425+
{
426+
if (cleanSegmentStart < str.Length)
437427
{
438-
output.Write(str);
428+
_jsonStringBuilder.Append(str, cleanSegmentStart, str.Length - cleanSegmentStart);
439429
}
440-
441-
output.Write('\"');
442-
return output.ToString();
443430
}
431+
else
432+
{
433+
_jsonStringBuilder.Append(str);
434+
}
435+
436+
_jsonStringBuilder.Append('\"');
437+
return _jsonStringBuilder.ToString();
444438
}
445439
}
446440
}

Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Formatting/ValueFormatterState.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ public string GetIndentation()
6363
return string.Empty;
6464
}
6565

66-
var totalSpaces = IndentLevel * IndentSize;
66+
var totalIndentUnits = IndentLevel * IndentSize;
6767
return UseSpacesForIndent
68-
? new string(' ', totalSpaces)
69-
: new string('\t', IndentLevel);
68+
? new string(' ', totalIndentUnits)
69+
: new string('\t', totalIndentUnits);
7070
}
7171
}
7272
}

Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class RichTextBoxSinkOptions
2626
{
2727
private const string DefaultOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}";
2828
private int _maxLogLines;
29+
private int _indentSize;
2930

3031
/// <summary>
3132
/// Creates a new collection of options that control the behavior and appearance of a
@@ -37,7 +38,7 @@ public class RichTextBoxSinkOptions
3738
/// <param name="outputTemplate">Serilog output template that controls textual formatting of each log event.</param>
3839
/// <param name="formatProvider">Optional culture-specific or custom formatting provider used when rendering scalar values; <c>null</c> for the invariant culture.</param>
3940
/// <param name="prettyPrintJson">When <c>true</c>, formats JSON values with indentation and line breaks for better readability. Defaults to <c>false</c>.</param>
40-
/// <param name="indentSize">Number of spaces per indentation level when pretty printing JSON. Defaults to 4.</param>
41+
/// <param name="indentSize">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.</param>
4142
/// <param name="useSpacesForIndent">When <c>true</c> (default), uses spaces for indentation; otherwise uses tabs.</param>
4243
public RichTextBoxSinkOptions(
4344
Theme theme,
@@ -80,7 +81,16 @@ public int MaxLogLines
8081

8182
public bool PrettyPrintJson { get; }
8283

83-
public int IndentSize { get; }
84+
public int IndentSize
85+
{
86+
get => _indentSize;
87+
private set => _indentSize = value switch
88+
{
89+
< 1 => 1,
90+
> 16 => 16,
91+
_ => value
92+
};
93+
}
8494

8595
public bool UseSpacesForIndent { get; }
8696
}

0 commit comments

Comments
 (0)