// This file is part of YamlDotNet - A .NET library for YAML. // Copyright (c) Antoine Aubry and contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Text.RegularExpressions; using YamlDotNet.Core.Events; using ParsingEvent = YamlDotNet.Core.Events.ParsingEvent; using TagDirective = YamlDotNet.Core.Tokens.TagDirective; using VersionDirective = YamlDotNet.Core.Tokens.VersionDirective; namespace YamlDotNet.Core { /// /// Emits YAML streams. /// public class Emitter : IEmitter { private static readonly Regex UriReplacer = new Regex(@"[^0-9A-Za-z_\-;?@=$~\\\)\]/:&+,\.\*\(\[!]", StandardRegexOptions.Compiled | RegexOptions.Singleline); private readonly TextWriter output; private readonly bool outputUsesUnicodeEncoding; private readonly int maxSimpleKeyLength; private readonly bool isCanonical; private readonly bool skipAnchorName; private readonly int bestIndent; private readonly int bestWidth; private EmitterState state; private readonly Stack states = new Stack(); private readonly Queue events = new Queue(); private readonly Stack indents = new Stack(); private readonly TagDirectiveCollection tagDirectives = new TagDirectiveCollection(); private int indent; private int flowLevel; private bool isMappingContext; private bool isSimpleKeyContext; private int column; private bool isWhitespace; private bool isIndentation; private readonly bool forceIndentLess; private bool isDocumentEndWritten; private readonly AnchorData anchorData = new AnchorData(); private readonly TagData tagData = new TagData(); private readonly ScalarData scalarData = new ScalarData(); private class AnchorData { public AnchorName Anchor; public bool IsAlias; } private class TagData { public string? Handle; public string? Suffix; } private class ScalarData { public string Value = string.Empty; public bool IsMultiline; public bool IsFlowPlainAllowed; public bool IsBlockPlainAllowed; public bool IsSingleQuotedAllowed; public bool IsBlockAllowed; public bool HasSingleQuotes; public ScalarStyle Style; } /// /// Initializes a new instance of the class. /// /// The where the emitter will write. public Emitter(TextWriter output) : this(output, EmitterSettings.Default) { } /// /// Initializes a new instance of the class. /// /// The where the emitter will write. /// The preferred indentation. public Emitter(TextWriter output, int bestIndent) : this(output, bestIndent, int.MaxValue) { } /// /// Initializes a new instance of the class. /// /// The where the emitter will write. /// The preferred indentation. /// The preferred text width. public Emitter(TextWriter output, int bestIndent, int bestWidth) : this(output, bestIndent, bestWidth, false) { } /// /// Initializes a new instance of the class. /// /// The where the emitter will write. /// The preferred indentation. /// The preferred text width. /// If true, write the output in canonical form. public Emitter(TextWriter output, int bestIndent, int bestWidth, bool isCanonical) : this(output, new EmitterSettings(bestIndent, bestWidth, isCanonical, 1024)) { } public Emitter(TextWriter output, EmitterSettings settings) { this.bestIndent = settings.BestIndent; this.bestWidth = settings.BestWidth; this.isCanonical = settings.IsCanonical; this.maxSimpleKeyLength = settings.MaxSimpleKeyLength; this.skipAnchorName = settings.SkipAnchorName; this.forceIndentLess = !settings.IndentSequences; this.output = output; this.outputUsesUnicodeEncoding = IsUnicode(output.Encoding); } /// /// Emit an evt. /// public void Emit(ParsingEvent @event) { events.Enqueue(@event); while (!NeedMoreEvents()) { var current = events.Peek(); try { AnalyzeEvent(current); StateMachine(current); } finally { // Only dequeue after calling state_machine because it checks how many events are in the queue. // Todo: well, move into StateMachine() then events.Dequeue(); } } } /// /// Check if we need to accumulate more events before emitting. /// /// We accumulate extra /// - 1 event for DOCUMENT-START /// - 2 events for SEQUENCE-START /// - 3 events for MAPPING-START /// private bool NeedMoreEvents() { if (events.Count == 0) { return true; } int accumulate; switch (events.Peek().Type) { case EventType.DocumentStart: accumulate = 1; break; case EventType.SequenceStart: accumulate = 2; break; case EventType.MappingStart: accumulate = 3; break; default: return false; } if (events.Count > accumulate) { return false; } var level = 0; foreach (var evt in events) { switch (evt.Type) { case EventType.DocumentStart: case EventType.SequenceStart: case EventType.MappingStart: ++level; break; case EventType.DocumentEnd: case EventType.SequenceEnd: case EventType.MappingEnd: --level; break; } if (level == 0) { return false; } } return true; } private void AnalyzeEvent(ParsingEvent evt) { anchorData.Anchor = AnchorName.Empty; tagData.Handle = null; tagData.Suffix = null; if (evt is AnchorAlias alias) { AnalyzeAnchor(alias.Value, true); return; } if (evt is NodeEvent nodeEvent) { if (evt is Scalar scalar) { AnalyzeScalar(scalar); } AnalyzeAnchor(nodeEvent.Anchor, false); if (!nodeEvent.Tag.IsEmpty && (isCanonical || nodeEvent.IsCanonical)) { AnalyzeTag(nodeEvent.Tag); } } } private void AnalyzeAnchor(AnchorName anchor, bool isAlias) { anchorData.Anchor = anchor; anchorData.IsAlias = isAlias; } private void AnalyzeScalar(Scalar scalar) { var value = scalar.Value; scalarData.Value = value; if (value.Length == 0) { if (scalar.Tag == "tag:yaml.org,2002:null") { scalarData.IsMultiline = false; scalarData.IsFlowPlainAllowed = false; scalarData.IsBlockPlainAllowed = true; scalarData.IsSingleQuotedAllowed = false; scalarData.IsBlockAllowed = false; } else { scalarData.IsMultiline = false; scalarData.IsFlowPlainAllowed = false; scalarData.IsBlockPlainAllowed = false; scalarData.IsSingleQuotedAllowed = true; scalarData.IsBlockAllowed = false; } return; } var flowIndicators = false; var blockIndicators = false; if (value.StartsWith("---", StringComparison.Ordinal) || value.StartsWith("...", StringComparison.Ordinal)) { flowIndicators = true; blockIndicators = true; } var buffer = new CharacterAnalyzer(new StringLookAheadBuffer(value)); var preceededByWhitespace = true; var followedByWhitespace = buffer.IsWhiteBreakOrZero(1); var leadingSpace = false; var leadingBreak = false; var trailingSpace = false; var trailingBreak = false; var leadingQuote = false; var breakSpace = false; var spaceBreak = false; var previousSpace = false; var previousBreak = false; var lineOfSpaces = false; var lineBreaks = false; var specialCharacters = !ValueIsRepresentableInOutputEncoding(value); var singleQuotes = false; var linesOfSpaces = false; var isFirst = true; while (!buffer.EndOfInput) { if (isFirst) { if (buffer.Check(@"#,[]{}&*!|>\""%@`'")) { flowIndicators = true; blockIndicators = true; leadingQuote = buffer.Check('\''); singleQuotes |= buffer.Check('\''); } if (buffer.Check("?:")) { flowIndicators = true; if (followedByWhitespace) { blockIndicators = true; } } if (buffer.Check('-') && followedByWhitespace) { flowIndicators = true; blockIndicators = true; } } else { if (buffer.Check(",?[]{}")) { flowIndicators = true; } if (buffer.Check(':')) { flowIndicators = true; if (followedByWhitespace) { blockIndicators = true; } } if (buffer.Check('#') && preceededByWhitespace) { flowIndicators = true; blockIndicators = true; } singleQuotes |= buffer.Check('\''); } if (!specialCharacters && !buffer.IsPrintable()) { specialCharacters = true; } if (buffer.IsBreak()) { lineBreaks = true; } if (buffer.IsSpace()) { if (isFirst) { leadingSpace = true; } if (buffer.Buffer.Position >= buffer.Buffer.Length - 1) { trailingSpace = true; } if (previousBreak) { breakSpace = true; lineOfSpaces = true; } previousSpace = true; previousBreak = false; } else if (buffer.IsBreak()) { if (isFirst) { leadingBreak = true; } if (buffer.Buffer.Position >= buffer.Buffer.Length - 1) { trailingBreak = true; } if (previousSpace) { spaceBreak = true; } if (lineOfSpaces) { linesOfSpaces = true; } previousSpace = false; previousBreak = true; } else { previousSpace = false; previousBreak = false; lineOfSpaces = false; } preceededByWhitespace = buffer.IsWhiteBreakOrZero(); buffer.Skip(1); if (!buffer.EndOfInput) { followedByWhitespace = buffer.IsWhiteBreakOrZero(1); } isFirst = false; } scalarData.IsFlowPlainAllowed = true; scalarData.IsBlockPlainAllowed = true; scalarData.IsSingleQuotedAllowed = true; scalarData.IsBlockAllowed = true; if (leadingSpace || leadingBreak || trailingSpace || trailingBreak || leadingQuote) { scalarData.IsFlowPlainAllowed = false; scalarData.IsBlockPlainAllowed = false; } if (trailingSpace) { scalarData.IsBlockAllowed = false; } if (breakSpace) { scalarData.IsFlowPlainAllowed = false; scalarData.IsBlockPlainAllowed = false; scalarData.IsSingleQuotedAllowed = false; } if (spaceBreak || specialCharacters) { scalarData.IsFlowPlainAllowed = false; scalarData.IsBlockPlainAllowed = false; scalarData.IsSingleQuotedAllowed = false; } if (linesOfSpaces) { scalarData.IsBlockAllowed = false; } scalarData.IsMultiline = lineBreaks; if (lineBreaks) { scalarData.IsFlowPlainAllowed = false; scalarData.IsBlockPlainAllowed = false; } if (flowIndicators) { scalarData.IsFlowPlainAllowed = false; } if (blockIndicators) { scalarData.IsBlockPlainAllowed = false; } scalarData.HasSingleQuotes = singleQuotes; } private bool ValueIsRepresentableInOutputEncoding(string value) { if (outputUsesUnicodeEncoding) { return true; } try { var encodedBytes = output.Encoding.GetBytes(value); var decodedString = output.Encoding.GetString(encodedBytes, 0, encodedBytes.Length); return decodedString.Equals(value); } catch (EncoderFallbackException) { return false; } catch (ArgumentOutOfRangeException) { return false; } } private bool IsUnicode(Encoding encoding) { return encoding is UTF8Encoding || encoding is UnicodeEncoding || encoding is UTF7Encoding; } private void AnalyzeTag(TagName tag) { tagData.Handle = tag.Value; foreach (var tagDirective in tagDirectives) { if (tag.Value.StartsWith(tagDirective.Prefix, StringComparison.Ordinal)) { tagData.Handle = tagDirective.Handle; tagData.Suffix = tag.Value.Substring(tagDirective.Prefix.Length); break; } } } private void StateMachine(ParsingEvent evt) { if (evt is Comment comment) { EmitComment(comment); return; } switch (state) { case EmitterState.StreamStart: EmitStreamStart(evt); break; case EmitterState.FirstDocumentStart: EmitDocumentStart(evt, true); break; case EmitterState.DocumentStart: EmitDocumentStart(evt, false); break; case EmitterState.DocumentContent: EmitDocumentContent(evt); break; case EmitterState.DocumentEnd: EmitDocumentEnd(evt); break; case EmitterState.FlowSequenceFirstItem: EmitFlowSequenceItem(evt, true); break; case EmitterState.FlowSequenceItem: EmitFlowSequenceItem(evt, false); break; case EmitterState.FlowMappingFirstKey: EmitFlowMappingKey(evt, true); break; case EmitterState.FlowMappingKey: EmitFlowMappingKey(evt, false); break; case EmitterState.FlowMappingSimpleValue: EmitFlowMappingValue(evt, true); break; case EmitterState.FlowMappingValue: EmitFlowMappingValue(evt, false); break; case EmitterState.BlockSequenceFirstItem: EmitBlockSequenceItem(evt, true); break; case EmitterState.BlockSequenceItem: EmitBlockSequenceItem(evt, false); break; case EmitterState.BlockMappingFirstKey: EmitBlockMappingKey(evt, true); break; case EmitterState.BlockMappingKey: EmitBlockMappingKey(evt, false); break; case EmitterState.BlockMappingSimpleValue: EmitBlockMappingValue(evt, true); break; case EmitterState.BlockMappingValue: EmitBlockMappingValue(evt, false); break; case EmitterState.StreamEnd: throw new YamlException("Expected nothing after STREAM-END"); default: throw new InvalidOperationException(); } } private void EmitComment(Comment comment) { if (comment.IsInline) { Write(' '); } else { WriteIndent(); } Write("# "); Write(comment.Value); WriteBreak(); isIndentation = true; } /// /// Expect STREAM-START. /// private void EmitStreamStart(ParsingEvent evt) { if (!(evt is StreamStart)) { throw new ArgumentException("Expected STREAM-START.", nameof(evt)); } indent = -1; column = 0; isWhitespace = true; isIndentation = true; state = EmitterState.FirstDocumentStart; } /// /// Expect DOCUMENT-START or STREAM-END. /// private void EmitDocumentStart(ParsingEvent evt, bool isFirst) { if (evt is DocumentStart documentStart) { var isImplicit = documentStart.IsImplicit && isFirst && !isCanonical; var documentTagDirectives = NonDefaultTagsAmong(documentStart.Tags); if (!isFirst && !isDocumentEndWritten && (documentStart.Version != null || documentTagDirectives.Count > 0)) { isDocumentEndWritten = false; WriteIndicator("...", true, false, false); WriteIndent(); } if (documentStart.Version != null) { AnalyzeVersionDirective(documentStart.Version); var documentVersion = documentStart.Version.Version; isImplicit = false; WriteIndicator("%YAML", true, false, false); WriteIndicator(string.Format(CultureInfo.InvariantCulture, "{0}.{1}", documentVersion.Major, documentVersion.Minor), true, false, false); WriteIndent(); } foreach (var tagDirective in documentTagDirectives) { AppendTagDirectiveTo(tagDirective, false, tagDirectives); } foreach (var tagDirective in Constants.DefaultTagDirectives) { AppendTagDirectiveTo(tagDirective, true, tagDirectives); } if (documentTagDirectives.Count > 0) { isImplicit = false; foreach (var tagDirective in Constants.DefaultTagDirectives) { AppendTagDirectiveTo(tagDirective, true, documentTagDirectives); } foreach (var tagDirective in documentTagDirectives) { WriteIndicator("%TAG", true, false, false); WriteTagHandle(tagDirective.Handle); WriteTagContent(tagDirective.Prefix, true); WriteIndent(); } } if (CheckEmptyDocument()) { isImplicit = false; } if (!isImplicit) { WriteIndent(); WriteIndicator("---", true, false, false); if (isCanonical) { WriteIndent(); } } state = EmitterState.DocumentContent; } else if (evt is StreamEnd) { state = EmitterState.StreamEnd; } else { throw new YamlException("Expected DOCUMENT-START or STREAM-END"); } } private TagDirectiveCollection NonDefaultTagsAmong(IEnumerable? tagCollection) { var directives = new TagDirectiveCollection(); if (tagCollection == null) { return directives; } foreach (var tagDirective in tagCollection) { AppendTagDirectiveTo(tagDirective, false, directives); } foreach (var tagDirective in Constants.DefaultTagDirectives) { directives.Remove(tagDirective); } return directives; } private void AnalyzeVersionDirective(VersionDirective versionDirective) { if (versionDirective.Version.Major != Constants.MajorVersion || versionDirective.Version.Minor > Constants.MinorVersion) { throw new YamlException("Incompatible %YAML directive"); } } private static void AppendTagDirectiveTo(TagDirective value, bool allowDuplicates, TagDirectiveCollection tagDirectives) { if (tagDirectives.Contains(value)) { if (!allowDuplicates) { throw new YamlException("Duplicate %TAG directive."); } } else { tagDirectives.Add(value); } } /// /// Expect the root node. /// private void EmitDocumentContent(ParsingEvent evt) { states.Push(EmitterState.DocumentEnd); EmitNode(evt, false, false); } /// /// Expect a node. /// private void EmitNode(ParsingEvent evt, bool isMapping, bool isSimpleKey) { isMappingContext = isMapping; isSimpleKeyContext = isSimpleKey; switch (evt.Type) { case EventType.Alias: EmitAlias(); break; case EventType.Scalar: EmitScalar(evt); break; case EventType.SequenceStart: EmitSequenceStart(evt); break; case EventType.MappingStart: EmitMappingStart(evt); break; default: throw new YamlException($"Expected SCALAR, SEQUENCE-START, MAPPING-START, or ALIAS, got {evt.Type}"); } } /// /// Expect ALIAS. /// private void EmitAlias() { ProcessAnchor(); state = states.Pop(); } /// /// Expect SCALAR. /// private void EmitScalar(ParsingEvent evt) { SelectScalarStyle(evt); ProcessAnchor(); ProcessTag(); IncreaseIndent(true, false); ProcessScalar(); indent = indents.Pop(); state = states.Pop(); } private void SelectScalarStyle(ParsingEvent evt) { var scalar = (Scalar)evt; var style = scalar.Style; var noTag = tagData.Handle == null && tagData.Suffix == null; if (noTag && !scalar.IsPlainImplicit && !scalar.IsQuotedImplicit) { throw new YamlException("Neither tag nor isImplicit flags are specified."); } if (style == ScalarStyle.Any) { style = scalarData.IsMultiline ? ScalarStyle.Folded : ScalarStyle.Plain; } if (isCanonical) { style = ScalarStyle.DoubleQuoted; } if (isSimpleKeyContext && scalarData.IsMultiline) { style = ScalarStyle.DoubleQuoted; } if (style == ScalarStyle.Plain) { if ((flowLevel != 0 && !scalarData.IsFlowPlainAllowed) || (flowLevel == 0 && !scalarData.IsBlockPlainAllowed)) { style = (scalarData.IsSingleQuotedAllowed && !scalarData.HasSingleQuotes) ? ScalarStyle.SingleQuoted : ScalarStyle.DoubleQuoted; } if (string.IsNullOrEmpty(scalarData.Value) && (flowLevel != 0 || isSimpleKeyContext)) { style = ScalarStyle.SingleQuoted; } if (noTag && !scalar.IsPlainImplicit) { style = ScalarStyle.SingleQuoted; } } if (style == ScalarStyle.SingleQuoted) { if (!scalarData.IsSingleQuotedAllowed) { style = ScalarStyle.DoubleQuoted; } } if (style == ScalarStyle.Literal || style == ScalarStyle.Folded) { if (!scalarData.IsBlockAllowed || flowLevel != 0 || isSimpleKeyContext) { style = ScalarStyle.DoubleQuoted; } } scalarData.Style = style; } private void ProcessScalar() { switch (scalarData.Style) { case ScalarStyle.Plain: WritePlainScalar(scalarData.Value, !isSimpleKeyContext); break; case ScalarStyle.SingleQuoted: WriteSingleQuotedScalar(scalarData.Value, !isSimpleKeyContext); break; case ScalarStyle.DoubleQuoted: WriteDoubleQuotedScalar(scalarData.Value, !isSimpleKeyContext); break; case ScalarStyle.Literal: WriteLiteralScalar(scalarData.Value); break; case ScalarStyle.Folded: WriteFoldedScalar(scalarData.Value); break; default: throw new InvalidOperationException(); } } #region Write scalar Methods private void WritePlainScalar(string value, bool allowBreaks) { if (!isWhitespace) { Write(' '); } var previousSpace = false; var previousBreak = false; for (var index = 0; index < value.Length; ++index) { var character = value[index]; if (IsSpace(character)) { if (allowBreaks && !previousSpace && column > bestWidth && index + 1 < value.Length && value[index + 1] != ' ') { WriteIndent(); } else { Write(character); } previousSpace = true; } else if (IsBreak(character, out var breakCharacter)) { if (!previousBreak && character == '\n') { WriteBreak(); } WriteBreak(breakCharacter); isIndentation = true; previousBreak = true; } else { if (previousBreak) { WriteIndent(); } Write(character); isIndentation = false; previousSpace = false; previousBreak = false; } } isWhitespace = false; isIndentation = false; } private void WriteSingleQuotedScalar(string value, bool allowBreaks) { WriteIndicator("'", true, false, false); var previousSpace = false; var previousBreak = false; for (var index = 0; index < value.Length; ++index) { var character = value[index]; if (character == ' ') { if (allowBreaks && !previousSpace && column > bestWidth && index != 0 && index + 1 < value.Length && value[index + 1] != ' ') { WriteIndent(); } else { Write(character); } previousSpace = true; } else if (IsBreak(character, out var breakCharacter)) { if (!previousBreak && character == '\n') { WriteBreak(); } WriteBreak(breakCharacter); isIndentation = true; previousBreak = true; } else { if (previousBreak) { WriteIndent(); } if (character == '\'') { Write(character); } Write(character); isIndentation = false; previousSpace = false; previousBreak = false; } } WriteIndicator("'", false, false, false); isWhitespace = false; isIndentation = false; } private void WriteDoubleQuotedScalar(string value, bool allowBreaks) { WriteIndicator("\"", true, false, false); var previousSpace = false; for (var index = 0; index < value.Length; ++index) { var character = value[index]; if (!IsPrintable(character) || IsBreak(character, out _) || character == '"' || character == '\\') { Write('\\'); switch (character) { case '\0': Write('0'); break; case '\x7': Write('a'); break; case '\x8': Write('b'); break; case '\x9': Write('t'); break; case '\xA': Write('n'); break; case '\xB': Write('v'); break; case '\xC': Write('f'); break; case '\xD': Write('r'); break; case '\x1B': Write('e'); break; case '\x22': Write('"'); break; case '\x5C': Write('\\'); break; case '\x85': Write('N'); break; case '\xA0': Write('_'); break; case '\x2028': Write('L'); break; case '\x2029': Write('P'); break; default: var code = (ushort)character; if (code <= 0xFF) { Write('x'); Write(code.ToString("X02", CultureInfo.InvariantCulture)); } else if (IsHighSurrogate(character)) { if (index + 1 < value.Length && IsLowSurrogate(value[index + 1])) { Write('U'); Write(char.ConvertToUtf32(character, value[index + 1]).ToString("X08", CultureInfo.InvariantCulture)); index++; } else { throw new SyntaxErrorException("While writing a quoted scalar, found an orphaned high surrogate."); } } else { Write('u'); Write(code.ToString("X04", CultureInfo.InvariantCulture)); } break; } previousSpace = false; } else if (character == ' ') { if (allowBreaks && !previousSpace && column > bestWidth && index > 0 && index + 1 < value.Length) { WriteIndent(); if (value[index + 1] == ' ') { Write('\\'); } } else { Write(character); } previousSpace = true; } else { Write(character); previousSpace = false; } } WriteIndicator("\"", false, false, false); isWhitespace = false; isIndentation = false; } private void WriteLiteralScalar(string value) { var previousBreak = true; WriteIndicator("|", true, false, false); WriteBlockScalarHints(value); WriteBreak(); isIndentation = true; isWhitespace = true; for (var i = 0; i < value.Length; ++i) { var character = value[i]; if (character == '\r' && (i + 1) < value.Length && value[i + 1] == '\n') { continue; } if (IsBreak(character, out var breakCharacter)) { WriteBreak(breakCharacter); isIndentation = true; previousBreak = true; } else { if (previousBreak) { WriteIndent(); } Write(character); isIndentation = false; previousBreak = false; } } } private void WriteFoldedScalar(string value) { var previousBreak = true; var leadingSpaces = true; WriteIndicator(">", true, false, false); WriteBlockScalarHints(value); WriteBreak(); isIndentation = true; isWhitespace = true; for (var i = 0; i < value.Length; ++i) { var character = value[i]; if (IsBreak(character, out var breakCharacter)) { if (character == '\r' && (i + 1) < value.Length && value[i + 1] == '\n') { continue; } if (!previousBreak && !leadingSpaces && breakCharacter == '\n') { var k = 0; while (i + k < value.Length && IsBreak(value[i + k], out _)) { ++k; } if (i + k < value.Length && !(IsBlank(value[i + k]) || IsBreak(value[i + k], out _))) { WriteBreak(); } } WriteBreak(breakCharacter); isIndentation = true; previousBreak = true; } else { if (previousBreak) { WriteIndent(); leadingSpaces = IsBlank(character); } if (!previousBreak && character == ' ' && i + 1 < value.Length && value[i + 1] != ' ' && column > bestWidth) { WriteIndent(); } else { Write(character); } isIndentation = false; previousBreak = false; } } } // Todo: isn't this what CharacterAnalyser is for? private static bool IsSpace(char character) { return character == ' '; } private static bool IsBreak(char character, out char breakChar) { switch (character) { case '\r': case '\n': case '\x85': breakChar = '\n'; return true; case '\x2028': case '\x2029': breakChar = character; return true; default: breakChar = '\0'; return false; } } private static bool IsBlank(char character) { return character == ' ' || character == '\t'; } private static bool IsPrintable(char character) { return character == '\x9' || character == '\xA' || character == '\xD' || (character >= '\x20' && character <= '\x7E') || character == '\x85' || (character >= '\xA0' && character <= '\xD7FF') || (character >= '\xE000' && character <= '\xFFFD'); } private static bool IsHighSurrogate(char c) { return 0xD800 <= c && c <= 0xDBFF; } private static bool IsLowSurrogate(char c) { return 0xDC00 <= c && c <= 0xDFFF; } #endregion /// /// Expect SEQUENCE-START. /// private void EmitSequenceStart(ParsingEvent evt) { ProcessAnchor(); ProcessTag(); var sequenceStart = (SequenceStart)evt; if (flowLevel != 0 || isCanonical || sequenceStart.Style == SequenceStyle.Flow || CheckEmptySequence()) { state = EmitterState.FlowSequenceFirstItem; } else { state = EmitterState.BlockSequenceFirstItem; } } /// /// Expect MAPPING-START. /// private void EmitMappingStart(ParsingEvent evt) { ProcessAnchor(); ProcessTag(); var mappingStart = (MappingStart)evt; if (flowLevel != 0 || isCanonical || mappingStart.Style == MappingStyle.Flow || CheckEmptyMapping()) { state = EmitterState.FlowMappingFirstKey; } else { state = EmitterState.BlockMappingFirstKey; } } private void ProcessAnchor() { if (!anchorData.Anchor.IsEmpty && !skipAnchorName) { WriteIndicator(anchorData.IsAlias ? "*" : "&", true, false, false); WriteAnchor(anchorData.Anchor); } } private void ProcessTag() { if (tagData.Handle == null && tagData.Suffix == null) { return; } if (tagData.Handle != null) { WriteTagHandle(tagData.Handle); if (tagData.Suffix != null) { WriteTagContent(tagData.Suffix, false); } } else { WriteIndicator("!<", true, false, false); WriteTagContent(tagData.Suffix!, false); WriteIndicator(">", false, false, false); } } /// /// Expect DOCUMENT-END. /// private void EmitDocumentEnd(ParsingEvent evt) { if (evt is DocumentEnd documentEnd) { WriteIndent(); if (!documentEnd.IsImplicit) { WriteIndicator("...", true, false, false); WriteIndent(); isDocumentEndWritten = true; } state = EmitterState.DocumentStart; tagDirectives.Clear(); } else { throw new YamlException("Expected DOCUMENT-END."); } } /// /// Expect a flow item node. /// private void EmitFlowSequenceItem(ParsingEvent evt, bool isFirst) { if (isFirst) { WriteIndicator("[", true, true, false); IncreaseIndent(true, false); ++flowLevel; } if (evt is SequenceEnd) { --flowLevel; indent = indents.Pop(); if (isCanonical && !isFirst) { WriteIndicator(",", false, false, false); WriteIndent(); } WriteIndicator("]", false, false, false); state = states.Pop(); return; } if (!isFirst) { WriteIndicator(",", false, false, false); } if (isCanonical || column > bestWidth) { WriteIndent(); } states.Push(EmitterState.FlowSequenceItem); EmitNode(evt, false, false); } /// /// Expect a flow key node. /// private void EmitFlowMappingKey(ParsingEvent evt, bool isFirst) { if (isFirst) { WriteIndicator("{", true, true, false); IncreaseIndent(true, false); ++flowLevel; } if (evt is MappingEnd) { --flowLevel; indent = indents.Pop(); if (isCanonical && !isFirst) { WriteIndicator(",", false, false, false); WriteIndent(); } WriteIndicator("}", false, false, false); state = states.Pop(); return; } if (!isFirst) { WriteIndicator(",", false, false, false); } if (isCanonical || column > bestWidth) { WriteIndent(); } if (!isCanonical && CheckSimpleKey()) { states.Push(EmitterState.FlowMappingSimpleValue); EmitNode(evt, true, true); } else { WriteIndicator("?", true, false, false); states.Push(EmitterState.FlowMappingValue); EmitNode(evt, true, false); } } /// /// Expect a flow value node. /// private void EmitFlowMappingValue(ParsingEvent evt, bool isSimple) { if (isSimple) { WriteIndicator(":", false, false, false); } else { if (isCanonical || column > bestWidth) { WriteIndent(); } WriteIndicator(":", true, false, false); } states.Push(EmitterState.FlowMappingKey); EmitNode(evt, true, false); } /// /// Expect a block item node. /// private void EmitBlockSequenceItem(ParsingEvent evt, bool isFirst) { if (isFirst) { IncreaseIndent(false, (isMappingContext && !isIndentation)); } if (evt is SequenceEnd) { indent = indents.Pop(); state = states.Pop(); return; } WriteIndent(); WriteIndicator("-", true, false, true); states.Push(EmitterState.BlockSequenceItem); EmitNode(evt, false, false); } /// /// Expect a block key node. /// private void EmitBlockMappingKey(ParsingEvent evt, bool isFirst) { if (isFirst) { IncreaseIndent(false, false); } if (evt is MappingEnd) { indent = indents.Pop(); state = states.Pop(); return; } WriteIndent(); if (CheckSimpleKey()) { states.Push(EmitterState.BlockMappingSimpleValue); EmitNode(evt, true, true); } else { WriteIndicator("?", true, false, true); states.Push(EmitterState.BlockMappingValue); EmitNode(evt, true, false); } } /// /// Expect a block value node. /// private void EmitBlockMappingValue(ParsingEvent evt, bool isSimple) { if (isSimple) { WriteIndicator(":", false, false, false); } else { WriteIndent(); WriteIndicator(":", true, false, true); } states.Push(EmitterState.BlockMappingKey); EmitNode(evt, true, false); } private void IncreaseIndent(bool isFlow, bool isIndentless) { indents.Push(indent); if (indent < 0) { indent = isFlow ? bestIndent : 0; } else if (!isIndentless || !forceIndentLess) { indent += bestIndent; } } #region Check Methods /// /// Check if the document content is an empty scalar. /// private bool CheckEmptyDocument() { var index = 0; foreach (var parsingEvent in events) { index++; if (index == 2) { if (parsingEvent is Scalar scalar) { return string.IsNullOrEmpty(scalar.Value); } break; } } return false; } /// /// Check if the next node can be expressed as a simple key. /// private bool CheckSimpleKey() { if (events.Count < 1) { return false; } int length; switch (events.Peek().Type) { case EventType.Alias: length = AnchorNameLength(anchorData.Anchor); break; case EventType.Scalar: if (scalarData.IsMultiline) { return false; } length = AnchorNameLength(anchorData.Anchor) + SafeStringLength(tagData.Handle) + SafeStringLength(tagData.Suffix) + SafeStringLength(scalarData.Value); break; case EventType.SequenceStart: if (!CheckEmptySequence()) { return false; } length = AnchorNameLength(anchorData.Anchor) + SafeStringLength(tagData.Handle) + SafeStringLength(tagData.Suffix); break; case EventType.MappingStart: if (!CheckEmptySequence()) { return false; } length = AnchorNameLength(anchorData.Anchor) + SafeStringLength(tagData.Handle) + SafeStringLength(tagData.Suffix); break; default: return false; } return length <= maxSimpleKeyLength; } private int AnchorNameLength(AnchorName value) { return value.IsEmpty ? 0 : value.Value.Length; } private int SafeStringLength(string? value) { return value == null ? 0 : value.Length; } private bool CheckEmptySequence() => CheckEmptyStructure(); private bool CheckEmptyMapping() => CheckEmptyStructure(); private bool CheckEmptyStructure() where TStart : NodeEvent where TEnd : ParsingEvent { if (events.Count < 2) { return false; } using var enumerator = events.GetEnumerator(); return enumerator.MoveNext() && enumerator.Current is TStart && enumerator.MoveNext() && enumerator.Current is TEnd; } #endregion #region Write Methods private void WriteBlockScalarHints(string value) { var analyzer = new CharacterAnalyzer(new StringLookAheadBuffer(value)); if (analyzer.IsSpace() || analyzer.IsBreak()) { var indentHint = bestIndent.ToString(CultureInfo.InvariantCulture); WriteIndicator(indentHint, false, false, false); } string? chompHint = null; if (value.Length == 0 || !analyzer.IsBreak(value.Length - 1)) { chompHint = "-"; } else if (value.Length >= 2 && analyzer.IsBreak(value.Length - 2)) { chompHint = "+"; } if (chompHint != null) { WriteIndicator(chompHint, false, false, false); } } private void WriteIndicator(string indicator, bool needWhitespace, bool whitespace, bool indentation) { if (needWhitespace && !isWhitespace) { Write(' '); } Write(indicator); isWhitespace = whitespace; isIndentation &= indentation; } private void WriteIndent() { var currentIndent = Math.Max(indent, 0); var isBreakRequired = !isIndentation || column > currentIndent || (column == currentIndent && !isWhitespace); if (isBreakRequired) { WriteBreak(); } while (column < currentIndent) { Write(' '); } isWhitespace = true; isIndentation = true; } private void WriteAnchor(AnchorName value) { Write(value.Value); isWhitespace = false; isIndentation = false; } private void WriteTagHandle(string value) { if (!isWhitespace) { Write(' '); } Write(value); isWhitespace = false; isIndentation = false; } private void WriteTagContent(string value, bool needsWhitespace) { if (needsWhitespace && !isWhitespace) { Write(' '); } Write(UrlEncode(value)); isWhitespace = false; isIndentation = false; } private string UrlEncode(string text) { return UriReplacer.Replace(text, delegate (Match match) { var buffer = new StringBuilder(); foreach (var toEncode in Encoding.UTF8.GetBytes(match.Value)) { buffer.AppendFormat("%{0:X02}", toEncode); } return buffer.ToString(); }); } private void Write(char value) { output.Write(value); ++column; } private void Write(string value) { output.Write(value); column += value.Length; } private void WriteBreak(char breakCharacter = '\n') { if (breakCharacter == '\n') { output.WriteLine(); } else { output.Write(breakCharacter); } column = 0; } #endregion } }