diff --git a/src/PAModel/PAConvert/Yaml/YamlWriter.cs b/src/PAModel/PAConvert/Yaml/YamlWriter.cs index d2900e92..62a68247 100644 --- a/src/PAModel/PAConvert/Yaml/YamlWriter.cs +++ b/src/PAModel/PAConvert/Yaml/YamlWriter.cs @@ -1,192 +1,215 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.IO; -namespace Microsoft.PowerPlatform.Formulas.Tools.Yaml +namespace Microsoft.PowerPlatform.Formulas.Tools.Yaml; + +/// +/// Helper to write out a safe subset of Yaml. +/// Notably, property values are always either multi-line escaped or +/// prefixed with '=' to block yaml from treating it as a yaml expression. +/// +public class YamlWriter : IDisposable { - /// - /// Helper to write out a safe subset of Yaml. - /// Notably, property values are always either multi-line escaped or - /// prefixed with '=' to block yaml from treating it as a yaml expression. - /// - public class YamlWriter - { - private readonly TextWriter _text; - private int _currentIndent; + private readonly TextWriter _textWriter; + private int _currentIndent; + private bool _isDisposed; + private const string Indent = " "; - private const string Indent = " "; + public YamlWriter(TextWriter text) + { + _textWriter = text ?? throw new ArgumentNullException(nameof(text)); + } - public YamlWriter(TextWriter text) - { - _text = text; - } + public void WriteStartObject(string propertyName) + { + WriteIndent(); - public void WriteStartObject(string propertyName) - { - WriteIndent(); + bool needsEscape = propertyName.IndexOfAny(new char[] { '\"', '\'' }) != -1; + if (needsEscape) + propertyName = $"\"{propertyName.Replace("\"", "\\\"")}\""; - bool needsEscape = propertyName.IndexOfAny(new char[] { '\"', '\'' }) != -1; - if (needsEscape) - propertyName = $"\"{propertyName.Replace("\"", "\\\"")}\""; + _textWriter.Write(propertyName); + _textWriter.WriteLine(":"); - _text.Write(propertyName); - _text.WriteLine(":"); + _currentIndent++; + } - _currentIndent++; + public void WriteEndObject() + { + _currentIndent--; + if (_currentIndent < 0) + { + throw new InvalidOperationException("No matching start object"); } - public void WriteEndObject() + } + + public void WriteProperty(string propertyName, bool value, bool includeEquals = true) + { + WriteIndent(); + _textWriter.Write(propertyName); + _textWriter.Write(": "); + if (includeEquals) { - _currentIndent--; - if (_currentIndent < 0) - { - throw new InvalidOperationException("No matching start object"); - } + _textWriter.Write("="); } + _textWriter.WriteLine(value ? "true" : "false"); + } - public void WriteProperty(string propertyName, bool value, bool includeEquals = true) + public void WriteProperty(string propertyName, int value, bool includeEquals = true) + { + WriteIndent(); + _textWriter.Write(propertyName); + _textWriter.Write(": "); + if (includeEquals) { - WriteIndent(); - _text.Write(propertyName); - _text.Write(": "); - if (includeEquals) - { - _text.Write("="); - } - _text.WriteLine(value ? "true" : "false"); + _textWriter.Write("="); } + _textWriter.WriteLine(value); + } - public void WriteProperty(string propertyName, int value, bool includeEquals = true) + public void WriteProperty(string propertyName, double value, bool includeEquals = true) + { + WriteIndent(); + _textWriter.Write(propertyName); + _textWriter.Write(": "); + if (includeEquals) { - WriteIndent(); - _text.Write(propertyName); - _text.Write(": "); - if (includeEquals) - { - _text.Write("="); - } - _text.WriteLine(value); + _textWriter.Write("="); } + _textWriter.WriteLine(value); + } - public void WriteProperty(string propertyName, double value, bool includeEquals = true) + /// + /// Safely write a property. Based on the value, will chose whether single-line (and prefix with an '=') + /// or multi-line and pick the right the escape. + /// + /// + /// + public void WriteProperty(string propertyName, string value, bool includeEquals = true) + { + if (value == null) { WriteIndent(); - _text.Write(propertyName); - _text.Write(": "); - if (includeEquals) - { - _text.Write("="); - } - _text.WriteLine(value); + _textWriter.Write(propertyName); + _textWriter.WriteLine(":"); + return; } - /// - /// Safely write a property. Based on the value, will chose whether single-line (and prefix with an '=') - /// or multi-line and pick the right the escape. - /// - /// - /// - public void WriteProperty(string propertyName, string value, bool includeEquals = true) - { - if (value == null) - { - WriteIndent(); - _text.Write(propertyName); - _text.WriteLine(":"); - return; - } + value = NormalizeNewlines(value); - value = NormalizeNewlines(value); + bool isSingleLine = value.IndexOfAny(new char[] { '#', '\n', ':' }) == -1; - bool isSingleLine = value.IndexOfAny(new char[] { '#', '\n', ':' }) == -1; + // For consistency, both single and multiline PA properties prefix with '='. + // Only single-line actually needs this - to avoid yaml's regular expression escaping. + if (includeEquals) + { + value = '=' + value; + } - // For consistency, both single and multiline PA properties prefix with '='. - // Only single-line actually needs this - to avoid yaml's regular expression escaping. - if (includeEquals) - { - value = '=' + value; - } + if (isSingleLine) + { + WriteIndent(); + _textWriter.Write(propertyName); + _textWriter.Write(": "); + _textWriter.WriteLine(value); + } + else + { + WriteIndent(); + _textWriter.Write(propertyName); + _textWriter.Write(": "); - if (isSingleLine) + int numNewlines = 0; + for (int i = value.Length - 1; i > 0; i--) { - WriteIndent(); - _text.Write(propertyName); - _text.Write(": "); - _text.WriteLine(value); - } - else - { - WriteIndent(); - _text.Write(propertyName); - _text.Write(": "); - - int numNewlines = 0; - for (int i = value.Length - 1; i > 0; i--) + if (value[i] == '\n') { - if (value[i] == '\n') - { - numNewlines++; - } - else - { - break; - } + numNewlines++; } - switch (numNewlines) + else { - case 0: _text.WriteLine("|-"); break; - case 1: _text.WriteLine("|"); break; - default: _text.WriteLine("|+"); break; + break; } + } + switch (numNewlines) + { + case 0: _textWriter.WriteLine("|-"); break; + case 1: _textWriter.WriteLine("|"); break; + default: _textWriter.WriteLine("|+"); break; + } - _currentIndent++; + _currentIndent++; - bool needIndent = true; - foreach (var ch in value) + bool needIndent = true; + foreach (var ch in value) + { + if (needIndent) { - if (needIndent) - { - WriteIndent(); - needIndent = false; - } - - if (ch == '\n') - { - _text.WriteLine(); - needIndent = true; - continue; - } - _text.Write(ch); + WriteIndent(); + needIndent = false; } - if (numNewlines == 0) + if (ch == '\n') { - _text.WriteLine(); + _textWriter.WriteLine(); + needIndent = true; + continue; } + _textWriter.Write(ch); + } - _currentIndent--; + if (numNewlines == 0) + { + _textWriter.WriteLine(); } + + _currentIndent--; } + } + + // Write a newline, for aesthics. since this is inbetween properties, + // it should get ignored on parse. + public void WriteNewline() + { + _textWriter.WriteLine(); + } - // Write a newline, for aesthics. since this is inbetween properties, - // it should get ignored on parse. - public void WriteNewline() + private void WriteIndent() + { + for (int i = 0; i < _currentIndent; i++) { - _text.WriteLine(); + _textWriter.Write(Indent); } + } - private void WriteIndent() + private string NormalizeNewlines(string x) + { + return x.Replace("\r\n", "\n").Replace("\r", "\n"); + } + + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) { - for (int i = 0; i < _currentIndent; i++) + if (disposing) { - _text.Write(Indent); + _textWriter?.Dispose(); } - } - private string NormalizeNewlines(string x) - { - return x.Replace("\r\n", "\n").Replace("\r", "\n"); + _isDisposed = true; } } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion }