1904 lines
58 KiB
C#
1904 lines
58 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// Emits YAML streams.
|
|
/// </summary>
|
|
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<EmitterState> states = new Stack<EmitterState>();
|
|
private readonly Queue<ParsingEvent> events = new Queue<ParsingEvent>();
|
|
private readonly Stack<int> indents = new Stack<int>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Emitter"/> class.
|
|
/// </summary>
|
|
/// <param name="output">The <see cref="TextWriter"/> where the emitter will write.</param>
|
|
public Emitter(TextWriter output)
|
|
: this(output, EmitterSettings.Default)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Emitter"/> class.
|
|
/// </summary>
|
|
/// <param name="output">The <see cref="TextWriter"/> where the emitter will write.</param>
|
|
/// <param name="bestIndent">The preferred indentation.</param>
|
|
public Emitter(TextWriter output, int bestIndent)
|
|
: this(output, bestIndent, int.MaxValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Emitter"/> class.
|
|
/// </summary>
|
|
/// <param name="output">The <see cref="TextWriter"/> where the emitter will write.</param>
|
|
/// <param name="bestIndent">The preferred indentation.</param>
|
|
/// <param name="bestWidth">The preferred text width.</param>
|
|
public Emitter(TextWriter output, int bestIndent, int bestWidth)
|
|
: this(output, bestIndent, bestWidth, false)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Emitter"/> class.
|
|
/// </summary>
|
|
/// <param name="output">The <see cref="TextWriter"/> where the emitter will write.</param>
|
|
/// <param name="bestIndent">The preferred indentation.</param>
|
|
/// <param name="bestWidth">The preferred text width.</param>
|
|
/// <param name="isCanonical">If true, write the output in canonical form.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emit an evt.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
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<StringLookAheadBuffer>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect STREAM-START.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect DOCUMENT-START or STREAM-END.
|
|
/// </summary>
|
|
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<TagDirective>? 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect the root node.
|
|
/// </summary>
|
|
private void EmitDocumentContent(ParsingEvent evt)
|
|
{
|
|
states.Push(EmitterState.DocumentEnd);
|
|
EmitNode(evt, false, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect a node.
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect ALIAS.
|
|
/// </summary>
|
|
private void EmitAlias()
|
|
{
|
|
ProcessAnchor();
|
|
state = states.Pop();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect SCALAR.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Expect SEQUENCE-START.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect MAPPING-START.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect DOCUMENT-END.
|
|
/// </summary>
|
|
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.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect a flow item node.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect a flow key node.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect a flow value node.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect a block item node.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect a block key node.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expect a block value node.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Check if the document content is an empty scalar.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the next node can be expressed as a simple key.
|
|
/// </summary>
|
|
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<SequenceStart, SequenceEnd>();
|
|
private bool CheckEmptyMapping() => CheckEmptyStructure<MappingStart, MappingEnd>();
|
|
|
|
private bool CheckEmptyStructure<TStart, TEnd>()
|
|
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<StringLookAheadBuffer>(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
|
|
}
|
|
}
|