├── .gitattributes ├── .gitignore ├── JsonFormatterPlus.sln ├── build ├── build.cmd └── build.ps1 ├── license.txt ├── readme.md ├── src └── JsonFormatterPlus │ ├── Internals │ ├── FormatterScopeState.cs │ ├── ICharacterStrategy.cs │ ├── JsonFormatterInternal.cs │ ├── JsonFormatterStrategyContext.cs │ └── Strategies │ │ ├── CloseBracketStrategy.cs │ │ ├── CloseSquareBracketStrategy.cs │ │ ├── ColonCharacterStrategy.cs │ │ ├── CommaCharacterStrategy.cs │ │ ├── DefaultCharacterStrategy.cs │ │ ├── DoubleQuoteStrategy.cs │ │ ├── OpenBracketStrategy.cs │ │ ├── OpenSquareBracketStrategy.cs │ │ ├── SingleQuoteStrategy.cs │ │ └── SkipWhileNotInStringStrategy.cs │ ├── JsonFormatter.cs │ └── JsonFormatterPlus.csproj └── test └── JsonFormatterPlus.Tests ├── JsonFormatterPlus.Tests.csproj └── JsonFormatterTests.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | artifacts/ 5 | build/tools/ 6 | -------------------------------------------------------------------------------- /JsonFormatterPlus.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonFormatterPlus", "src\JsonFormatterPlus\JsonFormatterPlus.csproj", "{D10288F8-7D1D-4B46-8A34-9D8A49EEB647}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonFormatterPlus.Tests", "test\JsonFormatterPlus.Tests\JsonFormatterPlus.Tests.csproj", "{D8BBDA57-EA52-4405-A825-DAFA5BEB32FE}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {D10288F8-7D1D-4B46-8A34-9D8A49EEB647}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {D10288F8-7D1D-4B46-8A34-9D8A49EEB647}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {D10288F8-7D1D-4B46-8A34-9D8A49EEB647}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {D10288F8-7D1D-4B46-8A34-9D8A49EEB647}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {D8BBDA57-EA52-4405-A825-DAFA5BEB32FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {D8BBDA57-EA52-4405-A825-DAFA5BEB32FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {D8BBDA57-EA52-4405-A825-DAFA5BEB32FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {D8BBDA57-EA52-4405-A825-DAFA5BEB32FE}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /build/build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0\build.ps1" 3 | -------------------------------------------------------------------------------- /build/build.ps1: -------------------------------------------------------------------------------- 1 | # Get the root project directory. 2 | $root = Resolve-Path (Join-Path $PSScriptRoot '..') 3 | 4 | # Set up the tools directory. 5 | $toolsDir = "$root\build\tools" 6 | if ((Test-Path -path $toolsDir) -eq $false) { 7 | New-Item $toolsDir -Type Directory | Out-Null 8 | } 9 | 10 | # Set up the output directory. 11 | $outputDir = "$root\artifacts" 12 | if ((Test-Path -path $outputDir) -eq $false) { 13 | New-Item $outputDir -Type Directory | Out-Null 14 | } 15 | 16 | # Find or download NuGet. 17 | $nuget = "$toolsDir\nuget.exe" 18 | if ((Test-Path $nuget) -eq $false) { 19 | Invoke-WebRequest -Uri https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile $nuget 20 | } 21 | 22 | # Find or download VSWhere. 23 | $vswhere = "$toolsDir\vswhere\tools\vswhere.exe" 24 | if ((Test-Path $vswhere) -eq $false) { 25 | & $nuget install vswhere -ExcludeVersion -OutputDirectory $toolsDir 26 | } 27 | 28 | # Find VS using VSWhere 29 | $vs = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild -property installationPath 30 | if ((Test-Path $vs) -eq $false) { 31 | throw 'Could not find VS installation' 32 | } 33 | 34 | # Locate MSBuild using resolved VS path. 35 | $msbuild = Resolve-Path "$vs\MSBuild\*\Bin\MSBuild.exe" 36 | if ((Test-Path $msbuild) -eq $false) { 37 | throw 'Could not find MSBuild' 38 | } 39 | 40 | # Set MSBuild alias. 41 | Set-Alias MSBuild $msbuild 42 | 43 | # Build and pack. 44 | MSBuild "/t:Restore,Build,Pack" "/p:Configuration=Release,OutputPath=$outputDir" "$root\src\JsonFormatterPlus\JsonFormatterPlus.csproj" 45 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2010 Mark Rogers 4 | Copyright (c) 2012-2018 Matthew King 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | JsonFormatterPlus 2 | ================= 3 | 4 | This library provides some simple JSON formatting / pretty printing functionality for .NET. 5 | It was originally based on Mark Rogers' [JsonPrettyPrinterPlus](http://www.markdavidrogers.com/oxitesample/Blog/json-pretty-printerbeautifier-library-for-net), but has evolved from there. 6 | 7 | ### Installation 8 | 9 | `PM> Install-Package JsonFormatterPlus` 10 | 11 | ### Example 12 | 13 | Pretty-printing a JSON string: 14 | 15 | ```csharp 16 | string formattedJson = JsonFormatter.Format(unformattedJson); 17 | ``` 18 | 19 | Minifying a JSON string: 20 | 21 | ```csharp 22 | string minifiedJson = JsonFormatter.Minify(json); 23 | ``` 24 | 25 | (I told you it was simple!) 26 | 27 | ### What sets this apart from JsonPrettyPrinterPlus? 28 | 29 | JsonPrettyPrinterPlus works very well. However, I wanted to make the project suitable for .NET 2.0, 3.0, and the various client profiles. This meant that the extension methods and the dependency on System.Web.Extensions had to go! 30 | A few extra 'quality of life' changes were also made, such as providing JSON minification, etc. 31 | 32 | ### License 33 | 34 | JsonFormatterPlus is distributed under the MIT license. 35 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/FormatterScopeState.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace JsonFormatterPlus.Internals 4 | { 5 | internal sealed class FormatterScopeState 6 | { 7 | public enum JsonScope 8 | { 9 | Object, 10 | Array 11 | } 12 | 13 | private readonly Stack _scopeStack = new Stack(); 14 | 15 | public bool IsTopTypeArray => _scopeStack.Count > 0 && _scopeStack.Peek() == JsonScope.Array; 16 | 17 | public int ScopeDepth => _scopeStack.Count; 18 | 19 | public void PushObjectContextOntoStack() 20 | { 21 | _scopeStack.Push(JsonScope.Object); 22 | } 23 | 24 | public JsonScope PopJsonType() 25 | { 26 | return _scopeStack.Pop(); 27 | } 28 | 29 | public void PushJsonArrayType() 30 | { 31 | _scopeStack.Push(JsonScope.Array); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/ICharacterStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace JsonFormatterPlus.Internals 2 | { 3 | internal interface ICharacterStrategy 4 | { 5 | void Execute(JsonFormatterStrategyContext context); 6 | 7 | char ForWhichCharacter { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/JsonFormatterInternal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using JsonFormatterPlus.Internals.Strategies; 4 | 5 | namespace JsonFormatterPlus.Internals 6 | { 7 | internal sealed class JsonFormatterInternal 8 | { 9 | private readonly JsonFormatterStrategyContext _context; 10 | 11 | public JsonFormatterInternal(JsonFormatterStrategyContext context) 12 | { 13 | _context = context; 14 | 15 | _context.ClearStrategies(); 16 | _context.AddCharacterStrategy(new OpenBracketStrategy()); 17 | _context.AddCharacterStrategy(new CloseBracketStrategy()); 18 | _context.AddCharacterStrategy(new OpenSquareBracketStrategy()); 19 | _context.AddCharacterStrategy(new CloseSquareBracketStrategy()); 20 | _context.AddCharacterStrategy(new SingleQuoteStrategy()); 21 | _context.AddCharacterStrategy(new DoubleQuoteStrategy()); 22 | _context.AddCharacterStrategy(new CommaStrategy()); 23 | _context.AddCharacterStrategy(new ColonCharacterStrategy()); 24 | _context.AddCharacterStrategy(new SkipWhileNotInStringStrategy('\n')); 25 | _context.AddCharacterStrategy(new SkipWhileNotInStringStrategy('\r')); 26 | _context.AddCharacterStrategy(new SkipWhileNotInStringStrategy('\t')); 27 | _context.AddCharacterStrategy(new SkipWhileNotInStringStrategy(' ')); 28 | } 29 | 30 | public string Format(string json) 31 | { 32 | if (json == null) 33 | { 34 | return String.Empty; 35 | } 36 | 37 | if (json.Trim() == String.Empty) 38 | { 39 | return String.Empty; 40 | } 41 | 42 | var input = new StringBuilder(json); 43 | var output = new StringBuilder(); 44 | 45 | PrettyPrintCharacter(input, output); 46 | 47 | return output.ToString(); 48 | } 49 | 50 | private void PrettyPrintCharacter(StringBuilder input, StringBuilder output) 51 | { 52 | for (int i = 0; i < input.Length; i++) 53 | { 54 | _context.PrettyPrintCharacter(input[i], output); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/JsonFormatterStrategyContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using JsonFormatterPlus.Internals.Strategies; 5 | 6 | namespace JsonFormatterPlus.Internals 7 | { 8 | internal sealed class JsonFormatterStrategyContext 9 | { 10 | private const string Space = " "; 11 | private const int SpacesPerIndent = 4; 12 | 13 | private string _indent = String.Empty; 14 | 15 | private char _currentCharacter; 16 | private char _previousChar; 17 | 18 | private StringBuilder _outputBuilder; 19 | 20 | private readonly FormatterScopeState _scopeState = new FormatterScopeState(); 21 | private readonly IDictionary _strategies = new Dictionary(); 22 | 23 | public string Indent 24 | { 25 | get 26 | { 27 | if (_indent == String.Empty) 28 | { 29 | InitializeIndent(); 30 | } 31 | 32 | return _indent; 33 | } 34 | } 35 | 36 | public bool IsInArrayScope => _scopeState.IsTopTypeArray; 37 | 38 | public bool IsProcessingVariableAssignment { get; set; } 39 | 40 | public bool IsProcessingDoubleQuoteInitiatedString { get; set; } 41 | 42 | public bool IsProcessingSingleQuoteInitiatedString { get; set; } 43 | 44 | public bool IsProcessingString => IsProcessingDoubleQuoteInitiatedString 45 | || IsProcessingSingleQuoteInitiatedString; 46 | 47 | public bool IsStart => _outputBuilder.Length == 0; 48 | 49 | public bool WasLastCharacterABackSlash => _previousChar == '\\'; 50 | 51 | private void InitializeIndent() 52 | { 53 | for (int i = 0; i < SpacesPerIndent; i++) 54 | { 55 | _indent += Space; 56 | } 57 | } 58 | 59 | private void AppendIndents(int indents) 60 | { 61 | for (int i = 0; i < indents; i++) 62 | { 63 | _outputBuilder.Append(Indent); 64 | } 65 | } 66 | 67 | public void PrettyPrintCharacter(char curChar, StringBuilder output) 68 | { 69 | _currentCharacter = curChar; 70 | 71 | ICharacterStrategy strategy = _strategies.ContainsKey(curChar) 72 | ? _strategies[curChar] 73 | : new DefaultCharacterStrategy(); 74 | 75 | _outputBuilder = output; 76 | 77 | strategy.Execute(this); 78 | 79 | _previousChar = curChar; 80 | } 81 | 82 | public void AppendCurrentChar() 83 | { 84 | _outputBuilder.Append(_currentCharacter); 85 | } 86 | 87 | public void AppendNewLine() 88 | { 89 | _outputBuilder.Append(Environment.NewLine); 90 | } 91 | 92 | public void BuildContextIndents() 93 | { 94 | AppendNewLine(); 95 | AppendIndents(_scopeState.ScopeDepth); 96 | } 97 | 98 | public void EnterObjectScope() 99 | { 100 | _scopeState.PushObjectContextOntoStack(); 101 | } 102 | 103 | public void CloseCurrentScope() 104 | { 105 | _scopeState.PopJsonType(); 106 | } 107 | 108 | public void EnterArrayScope() 109 | { 110 | _scopeState.PushJsonArrayType(); 111 | } 112 | 113 | public void AppendSpace() 114 | { 115 | _outputBuilder.Append(Space); 116 | } 117 | 118 | public void ClearStrategies() 119 | { 120 | _strategies.Clear(); 121 | } 122 | 123 | public void AddCharacterStrategy(ICharacterStrategy strategy) 124 | { 125 | _strategies[strategy.ForWhichCharacter] = strategy; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/Strategies/CloseBracketStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace JsonFormatterPlus.Internals.Strategies 2 | { 3 | internal sealed class CloseBracketStrategy : ICharacterStrategy 4 | { 5 | public char ForWhichCharacter => '}'; 6 | 7 | public void Execute(JsonFormatterStrategyContext context) 8 | { 9 | if (context.IsProcessingString) 10 | { 11 | context.AppendCurrentChar(); 12 | return; 13 | } 14 | 15 | PeformNonStringPrint(context); 16 | } 17 | 18 | private static void PeformNonStringPrint(JsonFormatterStrategyContext context) 19 | { 20 | context.CloseCurrentScope(); 21 | context.BuildContextIndents(); 22 | context.AppendCurrentChar(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/Strategies/CloseSquareBracketStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace JsonFormatterPlus.Internals.Strategies 2 | { 3 | internal sealed class CloseSquareBracketStrategy : ICharacterStrategy 4 | { 5 | public char ForWhichCharacter => ']'; 6 | 7 | public void Execute(JsonFormatterStrategyContext context) 8 | { 9 | if (context.IsProcessingString) 10 | { 11 | context.AppendCurrentChar(); 12 | return; 13 | } 14 | 15 | context.CloseCurrentScope(); 16 | context.BuildContextIndents(); 17 | context.AppendCurrentChar(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/Strategies/ColonCharacterStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace JsonFormatterPlus.Internals.Strategies 2 | { 3 | internal sealed class ColonCharacterStrategy : ICharacterStrategy 4 | { 5 | public char ForWhichCharacter => ':'; 6 | 7 | public void Execute(JsonFormatterStrategyContext context) 8 | { 9 | if (context.IsProcessingString) 10 | { 11 | context.AppendCurrentChar(); 12 | return; 13 | } 14 | 15 | context.IsProcessingVariableAssignment = true; 16 | context.AppendCurrentChar(); 17 | context.AppendSpace(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/Strategies/CommaCharacterStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace JsonFormatterPlus.Internals.Strategies 2 | { 3 | internal sealed class CommaStrategy : ICharacterStrategy 4 | { 5 | public char ForWhichCharacter => ','; 6 | 7 | public void Execute(JsonFormatterStrategyContext context) 8 | { 9 | context.AppendCurrentChar(); 10 | 11 | if (context.IsProcessingString) 12 | { 13 | return; 14 | } 15 | 16 | context.BuildContextIndents(); 17 | context.IsProcessingVariableAssignment = false; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/Strategies/DefaultCharacterStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JsonFormatterPlus.Internals.Strategies 4 | { 5 | internal sealed class DefaultCharacterStrategy : ICharacterStrategy 6 | { 7 | public char ForWhichCharacter => throw new InvalidOperationException("This strategy was not intended for any particular character."); 8 | 9 | public void Execute(JsonFormatterStrategyContext context) 10 | { 11 | context.AppendCurrentChar(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/Strategies/DoubleQuoteStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace JsonFormatterPlus.Internals.Strategies 2 | { 3 | internal sealed class DoubleQuoteStrategy : ICharacterStrategy 4 | { 5 | public char ForWhichCharacter => '"'; 6 | 7 | public void Execute(JsonFormatterStrategyContext context) 8 | { 9 | if (!context.IsProcessingSingleQuoteInitiatedString && !context.WasLastCharacterABackSlash) 10 | { 11 | context.IsProcessingDoubleQuoteInitiatedString = !context.IsProcessingDoubleQuoteInitiatedString; 12 | } 13 | 14 | context.AppendCurrentChar(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/Strategies/OpenBracketStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace JsonFormatterPlus.Internals.Strategies 2 | { 3 | internal sealed class OpenBracketStrategy : ICharacterStrategy 4 | { 5 | public char ForWhichCharacter => '{'; 6 | 7 | public void Execute(JsonFormatterStrategyContext context) 8 | { 9 | if (context.IsProcessingString) 10 | { 11 | context.AppendCurrentChar(); 12 | return; 13 | } 14 | 15 | context.AppendCurrentChar(); 16 | context.EnterObjectScope(); 17 | 18 | if (!IsBeginningOfNewLineAndIndentionLevel(context)) 19 | { 20 | return; 21 | } 22 | 23 | context.BuildContextIndents(); 24 | } 25 | 26 | private static bool IsBeginningOfNewLineAndIndentionLevel(JsonFormatterStrategyContext context) 27 | { 28 | return context.IsProcessingVariableAssignment 29 | || (!context.IsStart && !context.IsInArrayScope); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/Strategies/OpenSquareBracketStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace JsonFormatterPlus.Internals.Strategies 2 | { 3 | internal sealed class OpenSquareBracketStrategy : ICharacterStrategy 4 | { 5 | public char ForWhichCharacter => '['; 6 | 7 | public void Execute(JsonFormatterStrategyContext context) 8 | { 9 | context.AppendCurrentChar(); 10 | 11 | if (context.IsProcessingString) 12 | { 13 | return; 14 | } 15 | 16 | context.EnterArrayScope(); 17 | context.BuildContextIndents(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/Strategies/SingleQuoteStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace JsonFormatterPlus.Internals.Strategies 2 | { 3 | internal sealed class SingleQuoteStrategy : ICharacterStrategy 4 | { 5 | public char ForWhichCharacter => '\''; 6 | 7 | public void Execute(JsonFormatterStrategyContext context) 8 | { 9 | if (!context.IsProcessingDoubleQuoteInitiatedString && !context.WasLastCharacterABackSlash) 10 | { 11 | context.IsProcessingSingleQuoteInitiatedString = !context.IsProcessingSingleQuoteInitiatedString; 12 | } 13 | 14 | context.AppendCurrentChar(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/Internals/Strategies/SkipWhileNotInStringStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace JsonFormatterPlus.Internals.Strategies 2 | { 3 | internal sealed class SkipWhileNotInStringStrategy : ICharacterStrategy 4 | { 5 | public char ForWhichCharacter { get; } 6 | 7 | public SkipWhileNotInStringStrategy(char selectionCharacter) 8 | { 9 | ForWhichCharacter = selectionCharacter; 10 | } 11 | 12 | public void Execute(JsonFormatterStrategyContext context) 13 | { 14 | if (context.IsProcessingString) 15 | { 16 | context.AppendCurrentChar(); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/JsonFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using JsonFormatterPlus.Internals; 4 | 5 | namespace JsonFormatterPlus 6 | { 7 | /// 8 | /// Provides JSON formatting functionality. 9 | /// 10 | public static class JsonFormatter 11 | { 12 | /// 13 | /// Returns a 'pretty printed' version of the specified JSON string, formatted for human 14 | /// consumption. 15 | /// 16 | /// A valid JSON string. 17 | /// A 'pretty printed' version of the specified JSON string. 18 | public static string Format(string json) 19 | { 20 | if (json == null) 21 | { 22 | throw new ArgumentNullException(nameof(json)); 23 | } 24 | 25 | var context = new JsonFormatterStrategyContext(); 26 | var formatter = new JsonFormatterInternal(context); 27 | 28 | return formatter.Format(json); 29 | } 30 | 31 | /// 32 | /// Returns a 'minified' version of the specified JSON string, stripped of all 33 | /// non-essential characters. 34 | /// 35 | /// A valid JSON string. 36 | /// A 'minified' version of the specified JSON string. 37 | public static string Minify(string json) 38 | { 39 | if (json == null) 40 | { 41 | throw new ArgumentNullException(nameof(json)); 42 | } 43 | 44 | return Regex.Replace(json, "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/JsonFormatterPlus/JsonFormatterPlus.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | JsonFormatterPlus 5 | JsonFormatterPlus 6 | Simple JSON formatting/pretty-printing. 7 | Matthew King;Mark Rogers 8 | Copyright 2012-2018 Matthew King. 9 | https://github.com/MatthewKing/JsonFormatterPlus 10 | MIT 11 | json;format;prettyprint;pretty;print;minify 12 | 1.0.2 13 | 14 | 15 | 16 | net20;netstandard1.0;netstandard2.0 17 | true 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/JsonFormatterPlus.Tests/JsonFormatterPlus.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/JsonFormatterPlus.Tests/JsonFormatterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace JsonFormatterPlus.Tests 7 | { 8 | public sealed class JsonFormatterTests 9 | { 10 | #region Example data. 11 | 12 | // Source data from JSON.ORG: http://json.org/example.html 13 | // Formatted using JSONLint's formatter: http://jsonlint.com/ 14 | 15 | private static readonly string example1Minified = 16 | "{\"glossary\":{\"title\":\"example glossary\",\"GlossDiv\":{\"title\":\"S\",\"GlossList\":{\"GlossEntry\":{\"ID\":\"SGML\",\"SortAs\":\"SGML\",\"GlossTerm\":\"Standard Generalized Markup Language\",\"Acronym\":\"SGML\",\"Abbrev\":\"ISO 8879:1986\",\"GlossDef\":{\"para\":\"A meta-markup language,used to create markup languages such as DocBook.\",\"GlossSeeAlso\":[\"GML\",\"XML\"]},\"GlossSee\":\"markup\"}}}}}"; 17 | 18 | private static readonly string example1Formatted = 19 | "{" + Environment.NewLine + 20 | " \"glossary\": {" + Environment.NewLine + 21 | " \"title\": \"example glossary\"," + Environment.NewLine + 22 | " \"GlossDiv\": {" + Environment.NewLine + 23 | " \"title\": \"S\"," + Environment.NewLine + 24 | " \"GlossList\": {" + Environment.NewLine + 25 | " \"GlossEntry\": {" + Environment.NewLine + 26 | " \"ID\": \"SGML\"," + Environment.NewLine + 27 | " \"SortAs\": \"SGML\"," + Environment.NewLine + 28 | " \"GlossTerm\": \"Standard Generalized Markup Language\"," + Environment.NewLine + 29 | " \"Acronym\": \"SGML\"," + Environment.NewLine + 30 | " \"Abbrev\": \"ISO 8879:1986\"," + Environment.NewLine + 31 | " \"GlossDef\": {" + Environment.NewLine + 32 | " \"para\": \"A meta-markup language,used to create markup languages such as DocBook.\"," + Environment.NewLine + 33 | " \"GlossSeeAlso\": [" + Environment.NewLine + 34 | " \"GML\"," + Environment.NewLine + 35 | " \"XML\"" + Environment.NewLine + 36 | " ]" + Environment.NewLine + 37 | " }," + Environment.NewLine + 38 | " \"GlossSee\": \"markup\"" + Environment.NewLine + 39 | " }" + Environment.NewLine + 40 | " }" + Environment.NewLine + 41 | " }" + Environment.NewLine + 42 | " }" + Environment.NewLine + 43 | "}"; 44 | 45 | private static readonly string example2Minified = 46 | "{\"menu\":{\"id\":\"file\",\"value\":\"File\",\"popup\":{\"menuitem\":[{\"value\":\"New\",\"onclick\":\"CreateNewDoc()\"},{\"value\":\"Open\",\"onclick\":\"OpenDoc()\"},{\"value\":\"Close\",\"onclick\":\"CloseDoc()\"}]}}}"; 47 | 48 | private static readonly string example2Formatted = 49 | "{" + Environment.NewLine + 50 | " \"menu\": {" + Environment.NewLine + 51 | " \"id\": \"file\"," + Environment.NewLine + 52 | " \"value\": \"File\"," + Environment.NewLine + 53 | " \"popup\": {" + Environment.NewLine + 54 | " \"menuitem\": [" + Environment.NewLine + 55 | " {" + Environment.NewLine + 56 | " \"value\": \"New\"," + Environment.NewLine + 57 | " \"onclick\": \"CreateNewDoc()\"" + Environment.NewLine + 58 | " }," + Environment.NewLine + 59 | " {" + Environment.NewLine + 60 | " \"value\": \"Open\"," + Environment.NewLine + 61 | " \"onclick\": \"OpenDoc()\"" + Environment.NewLine + 62 | " }," + Environment.NewLine + 63 | " {" + Environment.NewLine + 64 | " \"value\": \"Close\"," + Environment.NewLine + 65 | " \"onclick\": \"CloseDoc()\"" + Environment.NewLine + 66 | " }" + Environment.NewLine + 67 | " ]" + Environment.NewLine + 68 | " }" + Environment.NewLine + 69 | " }" + Environment.NewLine + 70 | "}"; 71 | 72 | private static readonly string example3Minified = 73 | "{\"widget\":{\"debug\":\"on\",\"window\":{\"title\":\"Sample Konfabulator Widget\",\"name\":\"main_window\",\"width\":500,\"height\":500},\"image\":{\"src\":\"Images/Sun.png\",\"name\":\"sun1\",\"hOffset\":250,\"vOffset\":250,\"alignment\":\"center\"},\"text\":{\"data\":\"Click Here\",\"size\":36,\"style\":\"bold\",\"name\":\"text1\",\"hOffset\":250,\"vOffset\":100,\"alignment\":\"center\",\"onMouseUp\":\"sun1.opacity = (sun1.opacity / 100) * 90;\"}}}"; 74 | 75 | private static readonly string example3Formatted = 76 | "{" + Environment.NewLine + 77 | " \"widget\": {" + Environment.NewLine + 78 | " \"debug\": \"on\"," + Environment.NewLine + 79 | " \"window\": {" + Environment.NewLine + 80 | " \"title\": \"Sample Konfabulator Widget\"," + Environment.NewLine + 81 | " \"name\": \"main_window\"," + Environment.NewLine + 82 | " \"width\": 500," + Environment.NewLine + 83 | " \"height\": 500" + Environment.NewLine + 84 | " }," + Environment.NewLine + 85 | " \"image\": {" + Environment.NewLine + 86 | " \"src\": \"Images/Sun.png\"," + Environment.NewLine + 87 | " \"name\": \"sun1\"," + Environment.NewLine + 88 | " \"hOffset\": 250," + Environment.NewLine + 89 | " \"vOffset\": 250," + Environment.NewLine + 90 | " \"alignment\": \"center\"" + Environment.NewLine + 91 | " }," + Environment.NewLine + 92 | " \"text\": {" + Environment.NewLine + 93 | " \"data\": \"Click Here\"," + Environment.NewLine + 94 | " \"size\": 36," + Environment.NewLine + 95 | " \"style\": \"bold\"," + Environment.NewLine + 96 | " \"name\": \"text1\"," + Environment.NewLine + 97 | " \"hOffset\": 250," + Environment.NewLine + 98 | " \"vOffset\": 100," + Environment.NewLine + 99 | " \"alignment\": \"center\"," + Environment.NewLine + 100 | " \"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\"" + Environment.NewLine + 101 | " }" + Environment.NewLine + 102 | " }" + Environment.NewLine + 103 | "}"; 104 | 105 | private static readonly string example4Minified = 106 | "{\"menu\":{\"header\":\"SVG Viewer\",\"items\":[{\"id\":\"Open\"},{\"id\":\"OpenNew\",\"label\":\"Open New\"},null,{\"id\":\"ZoomIn\",\"label\":\"Zoom In\"},{\"id\":\"ZoomOut\",\"label\":\"Zoom Out\"},{\"id\":\"OriginalView\",\"label\":\"Original View\"},null,{\"id\":\"Quality\"},{\"id\":\"Pause\"},{\"id\":\"Mute\"},null,{\"id\":\"Find\",\"label\":\"Find...\"},{\"id\":\"FindAgain\",\"label\":\"Find Again\"},{\"id\":\"Copy\"},{\"id\":\"CopyAgain\",\"label\":\"Copy Again\"},{\"id\":\"CopySVG\",\"label\":\"Copy SVG\"},{\"id\":\"ViewSVG\",\"label\":\"View SVG\"},{\"id\":\"ViewSource\",\"label\":\"View Source\"},{\"id\":\"SaveAs\",\"label\":\"Save As\"},null,{\"id\":\"Help\"},{\"id\":\"About\",\"label\":\"About Adobe CVG Viewer...\"}]}}"; 107 | 108 | private static readonly string example4Formatted = 109 | "{" + Environment.NewLine + 110 | " \"menu\": {" + Environment.NewLine + 111 | " \"header\": \"SVG Viewer\"," + Environment.NewLine + 112 | " \"items\": [" + Environment.NewLine + 113 | " {" + Environment.NewLine + 114 | " \"id\": \"Open\"" + Environment.NewLine + 115 | " }," + Environment.NewLine + 116 | " {" + Environment.NewLine + 117 | " \"id\": \"OpenNew\"," + Environment.NewLine + 118 | " \"label\": \"Open New\"" + Environment.NewLine + 119 | " }," + Environment.NewLine + 120 | " null," + Environment.NewLine + 121 | " {" + Environment.NewLine + 122 | " \"id\": \"ZoomIn\"," + Environment.NewLine + 123 | " \"label\": \"Zoom In\"" + Environment.NewLine + 124 | " }," + Environment.NewLine + 125 | " {" + Environment.NewLine + 126 | " \"id\": \"ZoomOut\"," + Environment.NewLine + 127 | " \"label\": \"Zoom Out\"" + Environment.NewLine + 128 | " }," + Environment.NewLine + 129 | " {" + Environment.NewLine + 130 | " \"id\": \"OriginalView\"," + Environment.NewLine + 131 | " \"label\": \"Original View\"" + Environment.NewLine + 132 | " }," + Environment.NewLine + 133 | " null," + Environment.NewLine + 134 | " {" + Environment.NewLine + 135 | " \"id\": \"Quality\"" + Environment.NewLine + 136 | " }," + Environment.NewLine + 137 | " {" + Environment.NewLine + 138 | " \"id\": \"Pause\"" + Environment.NewLine + 139 | " }," + Environment.NewLine + 140 | " {" + Environment.NewLine + 141 | " \"id\": \"Mute\"" + Environment.NewLine + 142 | " }," + Environment.NewLine + 143 | " null," + Environment.NewLine + 144 | " {" + Environment.NewLine + 145 | " \"id\": \"Find\"," + Environment.NewLine + 146 | " \"label\": \"Find...\"" + Environment.NewLine + 147 | " }," + Environment.NewLine + 148 | " {" + Environment.NewLine + 149 | " \"id\": \"FindAgain\"," + Environment.NewLine + 150 | " \"label\": \"Find Again\"" + Environment.NewLine + 151 | " }," + Environment.NewLine + 152 | " {" + Environment.NewLine + 153 | " \"id\": \"Copy\"" + Environment.NewLine + 154 | " }," + Environment.NewLine + 155 | " {" + Environment.NewLine + 156 | " \"id\": \"CopyAgain\"," + Environment.NewLine + 157 | " \"label\": \"Copy Again\"" + Environment.NewLine + 158 | " }," + Environment.NewLine + 159 | " {" + Environment.NewLine + 160 | " \"id\": \"CopySVG\"," + Environment.NewLine + 161 | " \"label\": \"Copy SVG\"" + Environment.NewLine + 162 | " }," + Environment.NewLine + 163 | " {" + Environment.NewLine + 164 | " \"id\": \"ViewSVG\"," + Environment.NewLine + 165 | " \"label\": \"View SVG\"" + Environment.NewLine + 166 | " }," + Environment.NewLine + 167 | " {" + Environment.NewLine + 168 | " \"id\": \"ViewSource\"," + Environment.NewLine + 169 | " \"label\": \"View Source\"" + Environment.NewLine + 170 | " }," + Environment.NewLine + 171 | " {" + Environment.NewLine + 172 | " \"id\": \"SaveAs\"," + Environment.NewLine + 173 | " \"label\": \"Save As\"" + Environment.NewLine + 174 | " }," + Environment.NewLine + 175 | " null," + Environment.NewLine + 176 | " {" + Environment.NewLine + 177 | " \"id\": \"Help\"" + Environment.NewLine + 178 | " }," + Environment.NewLine + 179 | " {" + Environment.NewLine + 180 | " \"id\": \"About\"," + Environment.NewLine + 181 | " \"label\": \"About Adobe CVG Viewer...\"" + Environment.NewLine + 182 | " }" + Environment.NewLine + 183 | " ]" + Environment.NewLine + 184 | " }" + Environment.NewLine + 185 | "}"; 186 | 187 | #endregion 188 | 189 | [Fact] 190 | public void Format_JsonIsNull_ThrowsArgumentNullException() 191 | { 192 | Action act = () => JsonFormatter.Format(null); 193 | act.ShouldThrow(); 194 | } 195 | 196 | [Fact] 197 | public void Format_JsonIsEmptyString_ReturnsEmptyString() 198 | { 199 | JsonFormatter.Format(String.Empty).Should().BeEmpty(); 200 | } 201 | 202 | [Theory] 203 | [MemberData(nameof(Format_JsonIsValidButUnformatted_ReturnsFormattedJson_TestCaseData))] 204 | public void Format_JsonIsValidButUnformatted_ReturnsFormattedJson(string json, string formattedJson) 205 | { 206 | JsonFormatter.Format(json).Should().Be(formattedJson); 207 | } 208 | 209 | private static IEnumerable Format_JsonIsValidButUnformatted_ReturnsFormattedJson_TestCaseData() 210 | { 211 | yield return new object[] { example1Minified, example1Formatted }; 212 | yield return new object[] { example2Minified, example2Formatted }; 213 | yield return new object[] { example3Minified, example3Formatted }; 214 | yield return new object[] { example4Minified, example4Formatted }; 215 | } 216 | 217 | [Fact] 218 | public void Minify_JsonIsNull_ThrowsArgumentNullException() 219 | { 220 | Action act = () => JsonFormatter.Minify(null); 221 | act.ShouldThrow(); 222 | } 223 | 224 | [Fact] 225 | public void Minify_JsonIsEmpty_ReturnsEmptyString() 226 | { 227 | JsonFormatter.Minify(String.Empty).Should().BeEmpty(); 228 | } 229 | 230 | [Theory] 231 | [MemberData(nameof(Minify_JsonIsValidButNotMinified_ReturnsMinifiedJson_TestCaseData))] 232 | public void Minify_JsonIsValidButNotMinified_ReturnsMinifiedJson(string json, string minifiedJson) 233 | { 234 | JsonFormatter.Minify(json).Should().Be(minifiedJson); 235 | } 236 | 237 | private static IEnumerable Minify_JsonIsValidButNotMinified_ReturnsMinifiedJson_TestCaseData() 238 | { 239 | yield return new object[] { example1Formatted, example1Minified }; 240 | yield return new object[] { example2Formatted, example2Minified }; 241 | yield return new object[] { example3Formatted, example3Minified }; 242 | yield return new object[] { example4Formatted, example4Minified }; 243 | } 244 | } 245 | } 246 | --------------------------------------------------------------------------------