Code:
using System;
using System.Linq;
using System.Text;
using System.Web;
using Newtonsoft.Json.Linq;
namespace Conversion
{
/// <summary>
/// Defines interoperability functions for INI and JSON.
/// </summary>
public static class IniJsonInterop
{
/// <summary>
/// Performs JavaScript encoding on given string.
/// </summary>
/// <param name="value">The string to be encoded.</param>
public static string JavaScriptEncode(this string text)
{
return HttpUtility.JavaScriptStringEncode(text ?? string.Empty);
}
/// <summary>
/// Gets INI as JSON.
/// </summary>
/// <param name="value">INI source.</param>
/// <param name="indented"><c>true</c> to indent result JSON; otherwise, <c>false</c>.</param>
/// <param name="preserveComments"><c>true</c> to preserve comments; otherwise, <c>false</c>.</param>
/// <returns>Converted JSON.</returns>
public static string GetIniAsJson(string value, bool indented, bool preserveComments)
{
StringBuilder json = new();
string[] lines = value.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(line => line.Trim()).ToArray();
string section = null;
int kvpIndex = 0, commentIndex = 0;
void addComment(string comment, bool eol = false)
{
if (!preserveComments)
return;
json.Append($"{(kvpIndex > 0 ? "," : "")}\"@c{++commentIndex}{(eol ? "eol" : "")}\":\"{comment.Trim().JavaScriptEncode()}\"");
kvpIndex++;
}
string removeTrailingComment(string line)
{
// Remove any trailing comment from section line
int index = line.IndexOf(";", StringComparison.Ordinal);
if (index > -1)
{
addComment(line.Substring(index), true);
line = line.Substring(0, index).Trim();
}
return line;
}
json.Append("{");
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];
if (string.IsNullOrWhiteSpace(line) || line.StartsWith(";"))
{
addComment(line);
}
else if (line.StartsWith("["))
{
line = removeTrailingComment(line);
if (line.EndsWith("]"))
{
if (section is not null)
json.Append("},");
else if (commentIndex > 0)
json.Append(",");
section = line.Substring(1, line.Length - 2).Trim();
json.Append($"\"{section.JavaScriptEncode()}\":{{");
kvpIndex = commentIndex = 0;
}
else
{
throw new InvalidOperationException($"INI section has an invalid format: \"{lines[i]}\"");
}
}
else
{
line = removeTrailingComment(line);
string[] kvp = line.Split('=');
if (kvp.Length != 2)
throw new InvalidOperationException($"INI key-value entry has an invalid format: \"{lines[i]}\"");
json.Append($"{(kvpIndex > 0 ? "," : "")}\"{kvp[0].Trim().JavaScriptEncode()}\":\"{kvp[1].Trim().JavaScriptEncode()}\"");
kvpIndex++;
}
}
if (section is not null)
json.Append("}");
json.Append("}");
return indented ?
JToken.Parse(json.ToString()).ToString(Newtonsoft.Json.Formatting.Indented) :
json.ToString();
}
/// <summary>
/// Gets JSON as INI.
/// </summary>
/// <param name="value">JSON source.</param>
/// <param name="restoreComments"><c>true</c> to restore comments; otherwise, <c>false</c>.</param>
/// <returns>Converted INI.</returns>
public static string GetJsonAsIni(string value, bool restoreComments)
{
StringBuilder ini = new();
JObject json = JObject.Parse(value);
string eolComment = string.Empty;
void writeProperty(JProperty property)
{
if (property is null)
return;
if (property.Name.StartsWith("@c"))
{
if (restoreComments)
{
if (property.Name.EndsWith("eol"))
eolComment = $" {property.Value}";
else
ini.AppendLine(property.Value.ToString());
}
}
else
{
ini.AppendLine($"{property.Name}={property.Value}{eolComment}");
eolComment = string.Empty;
}
}
foreach (JProperty property in json.Properties())
{
if (property.Value.HasValues)
{
ini.AppendLine($"[{property.Name}]{eolComment}");
eolComment = string.Empty;
foreach (JToken kvp in property.Value)
writeProperty(kvp as JProperty);
}
else
{
writeProperty(property);
}
}
return ini.ToString();
}
}
}
Tests:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using static Conversion.IniJsonInterop;
namespace TestFunctions
{
[TestClass]
public class IniJsonTests
{
private static string iniInput =
@"; ------ PQube 3 from Powerside.
; ------ www.powerside.com
; ------ PQube 3 Version 3.8
rootKey=R00tVal
;----------------------------------------------------
[PQube_Information] ; End of section comment
;----------------------------------------------------
; ------ Assign a unique identifier for your PQube 3
PQube_ID=""(PQube_ID not set)"" ; End of value comment
; ------ Describe the place where your PQube 3 is installed
Location_Name=""(location not set)""
; ------ Optional additional information about your PQube 3
Note_1=""(note not set)""
Note_2=""(note not set)""
[NewSection]
; New section
NewKey=NewValue";
private static string jsonInput =
@"{
""@c1"": ""; ------ PQube 3 from Powerside."",
""@c2"": ""; ------ www.powerside.com"",
""@c3"": ""; ------ PQube 3 Version 3.8"",
""rootKey"": ""R00tVal"",
""@c4"": "";----------------------------------------------------"",
""@c5eol"": ""; End of section comment"",
""PQube_Information"": {
""@c1"": "";----------------------------------------------------"",
""@c2"": ""; ------ Assign a unique identifier for your PQube 3"",
""@c3eol"": ""; End of value comment"",
""PQube_ID"": ""\""(PQube_ID not set)\"""",
""@c4"": """",
""@c5"": ""; ------ Describe the place where your PQube 3 is installed"",
""Location_Name"": ""\""(location not set)\"""",
""@c6"": """",
""@c7"": ""; ------ Optional additional information about your PQube 3"",
""Note_1"": ""\""(note not set)\"""",
""Note_2"": ""\""(note not set)\""""
},
""NewSection"": {
""@c1"": ""; New section"",
""NewKey"": ""NewValue""
}
}";
[TestMethod]
public void TestIni2Json()
{
string json = GetIniAsJson(iniInput, true, true);
Console.WriteLine(json);
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(json), JToken.Parse(jsonInput)));
}
[TestMethod]
public void TestJson2Ini()
{
string ini = GetJsonAsIni(jsonInput,true);
Console.WriteLine(ini);
string[] getLines(string value) {
return value.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(line => line.Trim()).ToArray();
}
string[] sourceLines = getLines(ini);
string[] targetLines = getLines(iniInput + "\r\n");
Assert.IsTrue(sourceLines.CompareTo(targetLines) == 0);
}
}
internal static class CompareExtensions
{
public static int CompareTo<TSource>(this TSource[] array1, TSource[] array2, bool orderIsImportant = true) =>
array1.CompareTo(array2, Comparer<TSource>.Default, orderIsImportant);
private static int CompareTo(this Array array1, Array array2, IComparer comparer, bool orderIsImportant = true)
{
if (comparer is null)
throw new ArgumentNullException("comparer");
if (array1 is null && array2 is null)
return 0;
if (array1 is null)
return -1;
if (array2 is null)
return 1;
if (array1.Rank != 1 || array2.Rank != 1)
throw new ArgumentException("Cannot compare multidimensional arrays");
if (array1.Length != array2.Length)
return array1.Length.CompareTo(array2.Length);
if (!orderIsImportant)
{
array1 = array1.Cast<object>().ToArray();
array2 = array2.Cast<object>().ToArray();
Array.Sort(array1, comparer);
Array.Sort(array2, comparer);
}
int result = 0;
for (int i = 0; i < array1.Length; i++)
{
result = comparer.Compare(array1.GetValue(i), array2.GetValue(i));
if (result != 0)
break;
}
return result;
}
}
}