dyaml/source/dyaml/emitter.d

1697 lines
58 KiB
D
Raw Normal View History

// Copyright Ferdinand Majerech 2011.
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
/**
* YAML emitter.
* Code based on PyYAML: http://www.pyyaml.org
*/
module dyaml.emitter;
import std.algorithm;
import std.array;
import std.ascii;
import std.container;
import std.conv;
import std.exception;
import std.format;
import std.range;
import std.string;
import std.system;
import std.typecons;
import std.utf;
2016-03-16 23:43:58 +00:00
import dyaml.stream;
import dyaml.anchor;
import dyaml.encoding;
import dyaml.escapes;
import dyaml.event;
import dyaml.exception;
import dyaml.fastcharsearch;
import dyaml.flags;
import dyaml.linebreak;
import dyaml.queue;
import dyaml.style;
import dyaml.tag;
import dyaml.tagdirective;
package:
/**
* Exception thrown at Emitter errors.
*
* See_Also:
* YAMLException
*/
class EmitterException : YAMLException
{
mixin ExceptionCtors;
}
private alias EmitterException Error;
//Stores results of analysis of a scalar, determining e.g. what scalar style to use.
struct ScalarAnalysis
{
//Scalar itself.
string scalar;
///Analysis results.
Flags!("empty", "multiline", "allowFlowPlain", "allowBlockPlain",
"allowSingleQuoted", "allowDoubleQuoted", "allowBlock", "isNull") flags;
}
///Quickly determines if a character is a newline.
private mixin FastCharSearch!"\n\u0085\u2028\u2029"d newlineSearch_;
// override the canFind added by the FastCharSearch mixins
private alias canFind = std.algorithm.canFind;
//Emits YAML events into a file/stream.
struct Emitter
{
private:
alias dyaml.tagdirective.TagDirective TagDirective;
///Default tag handle shortcuts and replacements.
static TagDirective[] defaultTagDirectives_ =
[TagDirective("!", "!"), TagDirective("!!", "tag:yaml.org,2002:")];
///Stream to write to.
2016-03-16 23:43:58 +00:00
YStream stream_;
///Encoding can be overriden by STREAM-START.
Encoding encoding_ = Encoding.UTF_8;
///Stack of states.
Array!(void delegate()) states_;
///Current state.
void delegate() state_;
///Event queue.
Queue!Event events_;
///Event we're currently emitting.
Event event_;
///Stack of previous indentation levels.
Array!int indents_;
///Current indentation level.
int indent_ = -1;
///Level of nesting in flow context. If 0, we're in block context.
uint flowLevel_ = 0;
2012-09-13 23:16:05 +00:00
/// Describes context (where we are in the document).
enum Context
{
/// Root node of a document.
Root,
/// Sequence.
Sequence,
/// Mapping.
MappingNoSimpleKey,
/// Mapping, in a simple key.
MappingSimpleKey
}
/// Current context.
Context context_;
///Characteristics of the last emitted character:
///Line.
uint line_ = 0;
///Column.
uint column_ = 0;
///Whitespace character?
bool whitespace_ = true;
///indentation space, '-', '?', or ':'?
bool indentation_ = true;
///Does the document require an explicit document indicator?
bool openEnded_;
///Formatting details.
///Canonical scalar format?
bool canonical_;
///Best indentation width.
uint bestIndent_ = 2;
///Best text width.
uint bestWidth_ = 80;
///Best line break character/s.
LineBreak bestLineBreak_;
///Tag directive handle - prefix pairs.
TagDirective[] tagDirectives_;
///Anchor/alias to process.
string preparedAnchor_ = null;
///Tag to process.
string preparedTag_ = null;
///Analysis result of the current scalar.
ScalarAnalysis analysis_;
///Style of the current scalar.
ScalarStyle style_ = ScalarStyle.Invalid;
public:
@disable int opCmp(ref Emitter);
@disable bool opEquals(ref Emitter);
/**
* Construct an emitter.
*
2016-03-16 23:43:58 +00:00
* Params: stream = YStream to write to. Must be writable.
* canonical = Write scalars in canonical form?
* indent = Indentation width.
* lineBreak = Line break character/s.
*/
2016-03-16 23:43:58 +00:00
this(YStream stream, const bool canonical, const int indent, const int width,
2012-12-27 19:56:15 +00:00
const LineBreak lineBreak) @trusted
in{assert(stream.writeable, "Can't emit YAML to a non-writable stream");}
body
{
states_.reserve(32);
indents_.reserve(32);
stream_ = stream;
canonical_ = canonical;
state_ = &expectStreamStart;
if(indent > 1 && indent < 10){bestIndent_ = indent;}
if(width > bestIndent_ * 2) {bestWidth_ = width;}
bestLineBreak_ = lineBreak;
analysis_.flags.isNull = true;
}
///Destroy the emitter.
@trusted ~this()
{
stream_ = null;
2014-07-19 02:17:18 +00:00
states_.destroy();
events_.destroy();
indents_.destroy();
tagDirectives_.destroy();
tagDirectives_ = null;
2014-07-19 02:17:18 +00:00
preparedAnchor_.destroy();
preparedAnchor_ = null;
2014-07-19 02:17:18 +00:00
preparedTag_.destroy();
preparedTag_ = null;
}
///Emit an event. Throws EmitterException on error.
void emit(Event event) @trusted
{
events_.push(event);
while(!needMoreEvents())
{
event_ = events_.pop();
state_();
2014-07-19 02:17:18 +00:00
event_.destroy();
}
}
private:
///Pop and return the newest state in states_.
void delegate() popState() @trusted
{
enforce(states_.length > 0,
new YAMLException("Emitter: Need to pop a state but there are no states left"));
const result = states_.back;
states_.length = states_.length - 1;
return result;
}
///Pop and return the newest indent in indents_.
int popIndent() @trusted
{
enforce(indents_.length > 0,
new YAMLException("Emitter: Need to pop an indent level but there" ~
" are no indent levels left"));
const result = indents_.back;
indents_.length = indents_.length - 1;
return result;
}
///Write a string to the file/stream.
void writeString(const string str) @system
{
try final switch(encoding_)
{
case Encoding.UTF_8:
stream_.writeExact(str.ptr, str.length * char.sizeof);
break;
case Encoding.UTF_16:
const buffer = to!wstring(str);
stream_.writeExact(buffer.ptr, buffer.length * wchar.sizeof);
break;
case Encoding.UTF_32:
const buffer = to!dstring(str);
stream_.writeExact(buffer.ptr, buffer.length * dchar.sizeof);
break;
}
catch(Exception e)
{
throw new Error("Unable to write to stream: " ~ e.msg);
}
}
///In some cases, we wait for a few next events before emitting.
bool needMoreEvents() @trusted nothrow
{
if(events_.length == 0){return true;}
const event = events_.peek();
if(event.id == EventID.DocumentStart){return needEvents(1);}
if(event.id == EventID.SequenceStart){return needEvents(2);}
if(event.id == EventID.MappingStart) {return needEvents(3);}
return false;
}
///Determines if we need specified number of more events.
bool needEvents(in uint count) @system nothrow
{
int level = 0;
//Rather ugly, but good enough for now.
//Couldn't be bothered writing a range as events_ should eventually
//become a Phobos queue/linked list.
events_.startIteration();
events_.next();
while(!events_.iterationOver())
{
const event = events_.next();
static starts = [EventID.DocumentStart, EventID.SequenceStart, EventID.MappingStart];
static ends = [EventID.DocumentEnd, EventID.SequenceEnd, EventID.MappingEnd];
if(starts.canFind(event.id)) {++level;}
else if(ends.canFind(event.id)){--level;}
else if(event.id == EventID.StreamStart){level = -1;}
if(level < 0)
{
return false;
}
}
return events_.length < (count + 1);
}
///Increase indentation level.
2012-09-13 23:16:05 +00:00
void increaseIndent(const Flag!"flow" flow = No.flow, const bool indentless = false) @trusted
{
indents_ ~= indent_;
if(indent_ == -1)
{
indent_ = flow ? bestIndent_ : 0;
}
else if(!indentless)
{
indent_ += bestIndent_;
}
}
///Determines if the type of current event is as specified. Throws if no event.
bool eventTypeIs(in EventID id) const pure @trusted
{
enforce(!event_.isNull,
new Error("Expected an event, but no event is available."));
return event_.id == id;
}
//States.
//Stream handlers.
///Handle start of a file/stream.
void expectStreamStart() @trusted
{
enforce(eventTypeIs(EventID.StreamStart),
2016-03-16 23:43:58 +00:00
new Error("Expected YStreamStart, but got " ~ event_.idString));
encoding_ = event_.encoding;
writeStreamStart();
state_ = &expectDocumentStart!(Yes.first);
}
///Expect nothing, throwing if we still have something.
void expectNothing() const @trusted
{
throw new Error("Expected nothing, but got " ~ event_.idString);
}
//Document handlers.
///Handle start of a document.
void expectDocumentStart(Flag!"first" first)() @trusted
{
enforce(eventTypeIs(EventID.DocumentStart) || eventTypeIs(EventID.StreamEnd),
2016-03-16 23:43:58 +00:00
new Error("Expected DocumentStart or YStreamEnd, but got "
~ event_.idString));
if(event_.id == EventID.DocumentStart)
{
const YAMLVersion = event_.value;
auto tagDirectives = event_.tagDirectives;
if(openEnded_ && (YAMLVersion !is null || tagDirectives !is null))
{
2012-09-13 23:16:05 +00:00
writeIndicator("...", Yes.needWhitespace);
writeIndent();
}
if(YAMLVersion !is null)
{
writeVersionDirective(prepareVersion(YAMLVersion));
}
if(tagDirectives !is null)
{
tagDirectives_ = tagDirectives;
sort!"icmp(a.handle, b.handle) < 0"(tagDirectives_);
foreach(ref pair; tagDirectives_)
{
writeTagDirective(prepareTagHandle(pair.handle),
prepareTagPrefix(pair.prefix));
}
}
bool eq(ref TagDirective a, ref TagDirective b){return a.handle == b.handle;}
//Add any default tag directives that have not been overriden.
foreach(ref def; defaultTagDirectives_)
{
if(!std.algorithm.canFind!eq(tagDirectives_, def))
{
tagDirectives_ ~= def;
}
}
const implicit = first && !event_.explicitDocument && !canonical_ &&
YAMLVersion is null && tagDirectives is null &&
!checkEmptyDocument();
if(!implicit)
{
writeIndent();
2012-09-13 23:16:05 +00:00
writeIndicator("---", Yes.needWhitespace);
if(canonical_){writeIndent();}
}
2012-09-13 23:16:05 +00:00
state_ = &expectRootNode;
}
else if(event_.id == EventID.StreamEnd)
{
if(openEnded_)
{
2012-09-13 23:16:05 +00:00
writeIndicator("...", Yes.needWhitespace);
writeIndent();
}
writeStreamEnd();
state_ = &expectNothing;
}
}
///Handle end of a document.
void expectDocumentEnd() @trusted
{
enforce(eventTypeIs(EventID.DocumentEnd),
new Error("Expected DocumentEnd, but got " ~ event_.idString));
writeIndent();
if(event_.explicitDocument)
{
2012-09-13 23:16:05 +00:00
writeIndicator("...", Yes.needWhitespace);
writeIndent();
}
stream_.flush();
state_ = &expectDocumentStart!(No.first);
}
///Handle the root node of a document.
2012-09-13 23:16:05 +00:00
void expectRootNode() @trusted
{
states_ ~= &expectDocumentEnd;
2012-09-13 23:16:05 +00:00
expectNode(Context.Root);
}
2012-09-13 23:16:05 +00:00
///Handle a mapping node.
//
//Params: simpleKey = Are we in a simple key?
void expectMappingNode(const bool simpleKey = false)
{
2012-09-13 23:16:05 +00:00
expectNode(simpleKey ? Context.MappingSimpleKey : Context.MappingNoSimpleKey);
}
///Handle a sequence node.
void expectSequenceNode()
{
expectNode(Context.Sequence);
}
///Handle a new node. Context specifies where in the document we are.
void expectNode(const Context context) @trusted
{
context_ = context;
const flowCollection = event_.collectionStyle == CollectionStyle.Flow;
switch(event_.id)
{
case EventID.Alias: expectAlias(); break;
case EventID.Scalar:
processAnchor("&");
processTag();
expectScalar();
break;
case EventID.SequenceStart:
processAnchor("&");
processTag();
if(flowLevel_ > 0 || canonical_ || flowCollection || checkEmptySequence())
{
expectFlowSequence();
}
else
{
expectBlockSequence();
}
break;
case EventID.MappingStart:
processAnchor("&");
processTag();
if(flowLevel_ > 0 || canonical_ || flowCollection || checkEmptyMapping())
{
expectFlowMapping();
}
else
{
expectBlockMapping();
}
break;
default:
throw new Error("Expected Alias, Scalar, SequenceStart or " ~
"MappingStart, but got: " ~ event_.idString);
}
}
///Handle an alias.
void expectAlias() @trusted
{
enforce(!event_.anchor.isNull(), new Error("Anchor is not specified for alias"));
processAnchor("*");
state_ = popState();
}
///Handle a scalar.
void expectScalar() @trusted
{
2012-09-13 23:16:05 +00:00
increaseIndent(Yes.flow);
processScalar();
indent_ = popIndent();
state_ = popState();
}
//Flow sequence handlers.
///Handle a flow sequence.
void expectFlowSequence() @trusted
{
2012-09-13 23:16:05 +00:00
writeIndicator("[", Yes.needWhitespace, Yes.whitespace);
++flowLevel_;
2012-09-13 23:16:05 +00:00
increaseIndent(Yes.flow);
state_ = &expectFlowSequenceItem!(Yes.first);
}
///Handle a flow sequence item.
void expectFlowSequenceItem(Flag!"first" first)() @trusted
{
if(event_.id == EventID.SequenceEnd)
{
indent_ = popIndent();
--flowLevel_;
static if(!first) if(canonical_)
{
2012-09-13 23:16:05 +00:00
writeIndicator(",", No.needWhitespace);
writeIndent();
}
2012-09-13 23:16:05 +00:00
writeIndicator("]", No.needWhitespace);
state_ = popState();
return;
}
2012-09-13 23:16:05 +00:00
static if(!first){writeIndicator(",", No.needWhitespace);}
if(canonical_ || column_ > bestWidth_){writeIndent();}
states_ ~= &expectFlowSequenceItem!(No.first);
2012-09-13 23:16:05 +00:00
expectSequenceNode();
}
//Flow mapping handlers.
///Handle a flow mapping.
void expectFlowMapping() @trusted
{
2012-09-13 23:16:05 +00:00
writeIndicator("{", Yes.needWhitespace, Yes.whitespace);
++flowLevel_;
2012-09-13 23:16:05 +00:00
increaseIndent(Yes.flow);
state_ = &expectFlowMappingKey!(Yes.first);
}
///Handle a key in a flow mapping.
void expectFlowMappingKey(Flag!"first" first)() @trusted
{
if(event_.id == EventID.MappingEnd)
{
indent_ = popIndent();
--flowLevel_;
static if (!first) if(canonical_)
{
2012-09-13 23:16:05 +00:00
writeIndicator(",", No.needWhitespace);
writeIndent();
}
2012-09-13 23:16:05 +00:00
writeIndicator("}", No.needWhitespace);
state_ = popState();
return;
}
2012-09-13 23:16:05 +00:00
static if(!first){writeIndicator(",", No.needWhitespace);}
if(canonical_ || column_ > bestWidth_){writeIndent();}
if(!canonical_ && checkSimpleKey())
{
states_ ~= &expectFlowMappingSimpleValue;
2012-09-13 23:16:05 +00:00
expectMappingNode(true);
return;
}
2012-09-13 23:16:05 +00:00
writeIndicator("?", Yes.needWhitespace);
states_ ~= &expectFlowMappingValue;
2012-09-13 23:16:05 +00:00
expectMappingNode();
}
///Handle a simple value in a flow mapping.
void expectFlowMappingSimpleValue() @trusted
{
2012-09-13 23:16:05 +00:00
writeIndicator(":", No.needWhitespace);
states_ ~= &expectFlowMappingKey!(No.first);
2012-09-13 23:16:05 +00:00
expectMappingNode();
}
///Handle a complex value in a flow mapping.
void expectFlowMappingValue() @trusted
{
if(canonical_ || column_ > bestWidth_){writeIndent();}
2012-09-13 23:16:05 +00:00
writeIndicator(":", Yes.needWhitespace);
states_ ~= &expectFlowMappingKey!(No.first);
2012-09-13 23:16:05 +00:00
expectMappingNode();
}
//Block sequence handlers.
///Handle a block sequence.
void expectBlockSequence() @safe
{
2012-09-13 23:16:05 +00:00
const indentless = (context_ == Context.MappingNoSimpleKey ||
context_ == Context.MappingSimpleKey) && !indentation_;
increaseIndent(No.flow, indentless);
state_ = &expectBlockSequenceItem!(Yes.first);
}
///Handle a block sequence item.
void expectBlockSequenceItem(Flag!"first" first)() @trusted
{
static if(!first) if(event_.id == EventID.SequenceEnd)
{
indent_ = popIndent();
state_ = popState();
return;
}
writeIndent();
2012-09-13 23:16:05 +00:00
writeIndicator("-", Yes.needWhitespace, No.whitespace, Yes.indentation);
states_ ~= &expectBlockSequenceItem!(No.first);
2012-09-13 23:16:05 +00:00
expectSequenceNode();
}
//Block mapping handlers.
///Handle a block mapping.
void expectBlockMapping() @safe
{
2012-09-13 23:16:05 +00:00
increaseIndent(No.flow);
state_ = &expectBlockMappingKey!(Yes.first);
}
///Handle a key in a block mapping.
void expectBlockMappingKey(Flag!"first" first)() @trusted
{
static if(!first) if(event_.id == EventID.MappingEnd)
{
indent_ = popIndent();
state_ = popState();
return;
}
writeIndent();
if(checkSimpleKey())
{
states_ ~= &expectBlockMappingSimpleValue;
2012-09-13 23:16:05 +00:00
expectMappingNode(true);
return;
}
2012-09-13 23:16:05 +00:00
writeIndicator("?", Yes.needWhitespace, No.whitespace, Yes.indentation);
states_ ~= &expectBlockMappingValue;
2012-09-13 23:16:05 +00:00
expectMappingNode();
}
///Handle a simple value in a block mapping.
void expectBlockMappingSimpleValue() @trusted
{
2012-09-13 23:16:05 +00:00
writeIndicator(":", No.needWhitespace);
states_ ~= &expectBlockMappingKey!(No.first);
2012-09-13 23:16:05 +00:00
expectMappingNode();
}
///Handle a complex value in a block mapping.
void expectBlockMappingValue() @trusted
{
writeIndent();
2012-09-13 23:16:05 +00:00
writeIndicator(":", Yes.needWhitespace, No.whitespace, Yes.indentation);
states_ ~= &expectBlockMappingKey!(No.first);
2012-09-13 23:16:05 +00:00
expectMappingNode();
}
//Checkers.
///Check if an empty sequence is next.
bool checkEmptySequence() const @trusted pure nothrow
{
return event_.id == EventID.SequenceStart && events_.length > 0
&& events_.peek().id == EventID.SequenceEnd;
}
///Check if an empty mapping is next.
bool checkEmptyMapping() const @trusted pure nothrow
{
return event_.id == EventID.MappingStart && events_.length > 0
&& events_.peek().id == EventID.MappingEnd;
}
///Check if an empty document is next.
bool checkEmptyDocument() const @trusted pure nothrow
{
if(event_.id != EventID.DocumentStart || events_.length == 0)
{
return false;
}
const event = events_.peek();
const emptyScalar = event.id == EventID.Scalar && event.anchor.isNull() &&
event.tag.isNull() && event.implicit && event.value == "";
return emptyScalar;
}
///Check if a simple key is next.
bool checkSimpleKey() @trusted
{
uint length = 0;
const id = event_.id;
const scalar = id == EventID.Scalar;
const collectionStart = id == EventID.MappingStart ||
id == EventID.SequenceStart;
if((id == EventID.Alias || scalar || collectionStart)
&& !event_.anchor.isNull())
{
if(preparedAnchor_ is null)
{
preparedAnchor_ = prepareAnchor(event_.anchor);
}
length += preparedAnchor_.length;
}
if((scalar || collectionStart) && !event_.tag.isNull())
{
if(preparedTag_ is null){preparedTag_ = prepareTag(event_.tag);}
length += preparedTag_.length;
}
if(scalar)
{
if(analysis_.flags.isNull){analysis_ = analyzeScalar(event_.value);}
length += analysis_.scalar.length;
}
if(length >= 128){return false;}
return id == EventID.Alias ||
(scalar && !analysis_.flags.empty && !analysis_.flags.multiline) ||
checkEmptySequence() ||
checkEmptyMapping();
}
///Process and write a scalar.
void processScalar() @trusted
{
if(analysis_.flags.isNull){analysis_ = analyzeScalar(event_.value);}
if(style_ == ScalarStyle.Invalid)
{
style_ = chooseScalarStyle();
}
2012-09-13 23:16:05 +00:00
//if(analysis_.flags.multiline && (context_ != Context.MappingSimpleKey) &&
// ([ScalarStyle.Invalid, ScalarStyle.Plain, ScalarStyle.SingleQuoted, ScalarStyle.DoubleQuoted)
// .canFind(style_))
//{
// writeIndent();
//}
2012-09-13 23:16:05 +00:00
auto writer = ScalarWriter(this, analysis_.scalar,
context_ != Context.MappingSimpleKey);
with(writer) final switch(style_)
{
case ScalarStyle.Invalid: assert(false);
case ScalarStyle.DoubleQuoted: writeDoubleQuoted(); break;
case ScalarStyle.SingleQuoted: writeSingleQuoted(); break;
case ScalarStyle.Folded: writeFolded(); break;
case ScalarStyle.Literal: writeLiteral(); break;
case ScalarStyle.Plain: writePlain(); break;
}
analysis_.flags.isNull = true;
style_ = ScalarStyle.Invalid;
}
///Process and write an anchor/alias.
void processAnchor(const string indicator) @trusted
{
if(event_.anchor.isNull())
{
preparedAnchor_ = null;
return;
}
if(preparedAnchor_ is null)
{
preparedAnchor_ = prepareAnchor(event_.anchor);
}
if(preparedAnchor_ !is null && preparedAnchor_ != "")
{
2012-09-13 23:16:05 +00:00
writeIndicator(indicator, Yes.needWhitespace);
writeString(preparedAnchor_);
}
preparedAnchor_ = null;
}
///Process and write a tag.
void processTag() @trusted
{
Tag tag = event_.tag;
if(event_.id == EventID.Scalar)
{
if(style_ == ScalarStyle.Invalid){style_ = chooseScalarStyle();}
if((!canonical_ || tag.isNull()) &&
(style_ == ScalarStyle.Plain ? event_.implicit : event_.implicit_2))
{
preparedTag_ = null;
return;
}
if(event_.implicit && tag.isNull())
{
tag = Tag("!");
preparedTag_ = null;
}
}
else if((!canonical_ || tag.isNull()) && event_.implicit)
{
preparedTag_ = null;
return;
}
enforce(!tag.isNull(), new Error("Tag is not specified"));
if(preparedTag_ is null){preparedTag_ = prepareTag(tag);}
if(preparedTag_ !is null && preparedTag_ != "")
{
2012-09-13 23:16:05 +00:00
writeIndicator(preparedTag_, Yes.needWhitespace);
}
preparedTag_ = null;
}
///Determine style to write the current scalar in.
ScalarStyle chooseScalarStyle() @trusted
{
if(analysis_.flags.isNull){analysis_ = analyzeScalar(event_.value);}
const style = event_.scalarStyle;
const invalidOrPlain = style == ScalarStyle.Invalid || style == ScalarStyle.Plain;
const block = style == ScalarStyle.Literal || style == ScalarStyle.Folded;
const singleQuoted = style == ScalarStyle.SingleQuoted;
const doubleQuoted = style == ScalarStyle.DoubleQuoted;
const allowPlain = flowLevel_ > 0 ? analysis_.flags.allowFlowPlain
: analysis_.flags.allowBlockPlain;
//simple empty or multiline scalars can't be written in plain style
2012-09-13 23:16:05 +00:00
const simpleNonPlain = (context_ == Context.MappingSimpleKey) &&
(analysis_.flags.empty || analysis_.flags.multiline);
if(doubleQuoted || canonical_)
{
return ScalarStyle.DoubleQuoted;
}
if(invalidOrPlain && event_.implicit && !simpleNonPlain && allowPlain)
{
return ScalarStyle.Plain;
}
2012-09-13 23:16:05 +00:00
if(block && flowLevel_ == 0 && context_ != Context.MappingSimpleKey &&
analysis_.flags.allowBlock)
{
return style;
}
if((invalidOrPlain || singleQuoted) &&
analysis_.flags.allowSingleQuoted &&
2012-09-13 23:16:05 +00:00
!(context_ == Context.MappingSimpleKey && analysis_.flags.multiline))
{
return ScalarStyle.SingleQuoted;
}
return ScalarStyle.DoubleQuoted;
}
///Prepare YAML version string for output.
static string prepareVersion(const string YAMLVersion) @trusted
{
enforce(YAMLVersion.split(".")[0] == "1",
new Error("Unsupported YAML version: " ~ YAMLVersion));
return YAMLVersion;
}
///Encode an Unicode character for tag directive and write it to writer.
static void encodeChar(Writer)(ref Writer writer, in dchar c) @trusted
{
char[4] data;
const bytes = encode(data, c);
//For each byte add string in format %AB , where AB are hex digits of the byte.
foreach(const char b; data[0 .. bytes])
{
formattedWrite(writer, "%%%02X", cast(ubyte)b);
}
}
///Prepare tag directive handle for output.
static string prepareTagHandle(const string handle) @trusted
{
enforce(handle !is null && handle != "",
new Error("Tag handle must not be empty"));
if(handle.length > 1) foreach(const dchar c; handle[1 .. $ - 1])
{
enforce(isAlphaNum(c) || "-_"d.canFind(c),
new Error("Invalid character: " ~ to!string(c) ~
" in tag handle " ~ handle));
}
return handle;
}
///Prepare tag directive prefix for output.
static string prepareTagPrefix(const string prefix) @trusted
{
enforce(prefix !is null && prefix != "",
new Error("Tag prefix must not be empty"));
auto appender = appender!string();
const offset = prefix[0] == '!' ? 1 : 0;
size_t start = 0;
size_t end = 0;
foreach(const size_t i, const dchar c; prefix)
{
const size_t idx = i + offset;
if(isAlphaNum(c) || "-;/?:@&=+$,_.!~*\'()[]%"d.canFind(c))
{
end = idx + 1;
continue;
}
if(start < idx){appender.put(prefix[start .. idx]);}
start = end = idx + 1;
encodeChar(appender, c);
}
end = min(end, prefix.length);
if(start < end){appender.put(prefix[start .. end]);}
return appender.data;
}
///Prepare tag for output.
string prepareTag(in Tag tag) @trusted
{
enforce(!tag.isNull(), new Error("Tag must not be empty"));
string tagString = tag.get;
if(tagString == "!"){return tagString;}
string handle = null;
string suffix = tagString;
//Sort lexicographically by prefix.
sort!"icmp(a.prefix, b.prefix) < 0"(tagDirectives_);
foreach(ref pair; tagDirectives_)
{
auto prefix = pair.prefix;
if(tagString.startsWith(prefix) &&
(prefix != "!" || prefix.length < tagString.length))
{
handle = pair.handle;
suffix = tagString[prefix.length .. $];
}
}
auto appender = appender!string();
appender.put(handle !is null && handle != "" ? handle : "!<");
size_t start = 0;
size_t end = 0;
foreach(const dchar c; suffix)
{
if(isAlphaNum(c) || "-;/?:@&=+$,_.~*\'()[]"d.canFind(c) ||
(c == '!' && handle != "!"))
{
++end;
continue;
}
if(start < end){appender.put(suffix[start .. end]);}
start = end = end + 1;
encodeChar(appender, c);
}
if(start < end){appender.put(suffix[start .. end]);}
if(handle is null || handle == ""){appender.put(">");}
return appender.data;
}
///Prepare anchor for output.
2012-09-13 23:16:05 +00:00
static string prepareAnchor(const Anchor anchor) @trusted
{
enforce(!anchor.isNull() && anchor.get != "",
new Error("Anchor must not be empty"));
const str = anchor.get;
foreach(const dchar c; str)
{
enforce(isAlphaNum(c) || "-_"d.canFind(c),
new Error("Invalid character: " ~ to!string(c) ~ " in anchor: " ~ str));
}
return str;
}
///Analyze specifed scalar and return the analysis result.
static ScalarAnalysis analyzeScalar(string scalar) @safe
{
ScalarAnalysis analysis;
analysis.flags.isNull = false;
analysis.scalar = scalar;
//Empty scalar is a special case.
with(analysis.flags) if(scalar is null || scalar == "")
{
empty = true;
multiline = false;
allowFlowPlain = false;
allowBlockPlain = true;
allowSingleQuoted = true;
allowDoubleQuoted = true;
allowBlock = false;
return analysis;
}
//Indicators and special characters (All false by default).
bool blockIndicators, flowIndicators, lineBreaks, specialCharacters;
//Important whitespace combinations (All false by default).
bool leadingSpace, leadingBreak, trailingSpace, trailingBreak,
breakSpace, spaceBreak;
//Check document indicators.
if(scalar.startsWith("---", "..."))
{
blockIndicators = flowIndicators = true;
}
//First character or preceded by a whitespace.
bool preceededByWhitespace = true;
//Last character or followed by a whitespace.
bool followedByWhitespace = scalar.length == 1 ||
" \t\0\n\r\u0085\u2028\u2029"d.canFind(scalar[1]);
//The previous character is a space/break (false by default).
bool previousSpace, previousBreak;
foreach(const size_t index, const dchar c; scalar)
{
mixin FastCharSearch!("#,[]{}&*!|>\'\"%@`"d, 128) specialCharSearch;
mixin FastCharSearch!(",?[]{}"d, 128) flowIndicatorSearch;
//Check for indicators.
if(index == 0)
{
//Leading indicators are special characters.
if(specialCharSearch.canFind(c))
{
flowIndicators = blockIndicators = true;
}
if(':' == c || '?' == c)
{
flowIndicators = true;
if(followedByWhitespace){blockIndicators = true;}
}
if(c == '-' && followedByWhitespace)
{
flowIndicators = blockIndicators = true;
}
}
else
{
//Some indicators cannot appear within a scalar as well.
if(flowIndicatorSearch.canFind(c)){flowIndicators = true;}
if(c == ':')
{
flowIndicators = true;
if(followedByWhitespace){blockIndicators = true;}
}
if(c == '#' && preceededByWhitespace)
{
flowIndicators = blockIndicators = true;
}
}
//Check for line breaks, special, and unicode characters.
if(newlineSearch_.canFind(c)){lineBreaks = true;}
if(!(c == '\n' || (c >= '\x20' && c <= '\x7E')) &&
!((c == '\u0085' || (c >= '\xA0' && c <= '\uD7FF') ||
(c >= '\uE000' && c <= '\uFFFD')) && c != '\uFEFF'))
{
specialCharacters = true;
}
//Detect important whitespace combinations.
if(c == ' ')
{
if(index == 0){leadingSpace = true;}
if(index == scalar.length - 1){trailingSpace = true;}
if(previousBreak){breakSpace = true;}
previousSpace = true;
previousBreak = false;
}
else if(newlineSearch_.canFind(c))
{
if(index == 0){leadingBreak = true;}
if(index == scalar.length - 1){trailingBreak = true;}
if(previousSpace){spaceBreak = true;}
previousSpace = false;
previousBreak = true;
}
else
{
previousSpace = previousBreak = false;
}
mixin FastCharSearch! "\0\n\r\u0085\u2028\u2029 \t"d spaceSearch;
//Prepare for the next character.
preceededByWhitespace = spaceSearch.canFind(c);
followedByWhitespace = index + 2 >= scalar.length ||
spaceSearch.canFind(scalar[index + 2]);
}
with(analysis.flags)
{
//Let's decide what styles are allowed.
allowFlowPlain = allowBlockPlain = allowSingleQuoted
= allowDoubleQuoted = allowBlock = true;
//Leading and trailing whitespaces are bad for plain scalars.
if(leadingSpace || leadingBreak || trailingSpace || trailingBreak)
{
allowFlowPlain = allowBlockPlain = false;
}
//We do not permit trailing spaces for block scalars.
if(trailingSpace){allowBlock = false;}
//Spaces at the beginning of a new line are only acceptable for block
//scalars.
if(breakSpace)
{
allowFlowPlain = allowBlockPlain = allowSingleQuoted = false;
}
//Spaces followed by breaks, as well as special character are only
//allowed for double quoted scalars.
if(spaceBreak || specialCharacters)
{
allowFlowPlain = allowBlockPlain = allowSingleQuoted = allowBlock = false;
}
//Although the plain scalar writer supports breaks, we never emit
//multiline plain scalars.
if(lineBreaks){allowFlowPlain = allowBlockPlain = false;}
//Flow indicators are forbidden for flow plain scalars.
if(flowIndicators){allowFlowPlain = false;}
//Block indicators are forbidden for block plain scalars.
if(blockIndicators){allowBlockPlain = false;}
empty = false;
multiline = lineBreaks;
}
return analysis;
}
//Writers.
///Start the YAML stream (write the unicode byte order mark).
void writeStreamStart() @system
{
immutable(ubyte)[] bom;
//Write BOM (always, even for UTF-8)
final switch(encoding_)
{
case Encoding.UTF_8:
bom = ByteOrderMarks[BOM.UTF8];
break;
case Encoding.UTF_16:
bom = std.system.endian == Endian.littleEndian
? ByteOrderMarks[BOM.UTF16LE]
: ByteOrderMarks[BOM.UTF16BE];
break;
case Encoding.UTF_32:
bom = std.system.endian == Endian.littleEndian
? ByteOrderMarks[BOM.UTF32LE]
: ByteOrderMarks[BOM.UTF32BE];
break;
}
enforce(stream_.write(bom) == bom.length, new Error("Unable to write to stream"));
}
///End the YAML stream.
void writeStreamEnd() @system {stream_.flush();}
///Write an indicator (e.g. ":", "[", ">", etc.).
2012-09-13 23:16:05 +00:00
void writeIndicator(const string indicator,
const Flag!"needWhitespace" needWhitespace,
const Flag!"whitespace" whitespace = No.whitespace,
const Flag!"indentation" indentation = No.indentation) @system
{
const bool prefixSpace = !whitespace_ && needWhitespace;
2012-09-13 23:16:05 +00:00
whitespace_ = whitespace;
indentation_ = indentation_ && indentation;
2012-09-13 23:16:05 +00:00
openEnded_ = false;
column_ += indicator.length;
if(prefixSpace)
{
++column_;
writeString(" ");
}
writeString(indicator);
}
///Write indentation.
void writeIndent() @system
{
const indent = indent_ == -1 ? 0 : indent_;
if(!indentation_ || column_ > indent || (column_ == indent && !whitespace_))
{
writeLineBreak();
}
if(column_ < indent)
{
whitespace_ = true;
//Used to avoid allocation of arbitrary length strings.
static immutable spaces = " ";
size_t numSpaces = indent - column_;
column_ = indent;
while(numSpaces >= spaces.length)
{
writeString(spaces);
numSpaces -= spaces.length;
}
writeString(spaces[0 .. numSpaces]);
}
}
///Start new line.
void writeLineBreak(const string data = null) @system
{
whitespace_ = indentation_ = true;
++line_;
column_ = 0;
writeString(data is null ? lineBreak(bestLineBreak_) : data);
}
///Write a YAML version directive.
void writeVersionDirective(const string versionText) @system
{
writeString("%YAML ");
writeString(versionText);
writeLineBreak();
}
///Write a tag directive.
void writeTagDirective(const string handle, const string prefix) @system
{
writeString("%TAG ");
writeString(handle);
writeString(" ");
writeString(prefix);
writeLineBreak();
}
}
private:
///RAII struct used to write out scalar values.
struct ScalarWriter
{
invariant()
{
assert(emitter_.bestIndent_ > 0 && emitter_.bestIndent_ < 10,
"Emitter bestIndent must be 1 to 9 for one-character indent hint");
}
private:
@disable int opCmp(ref Emitter);
@disable bool opEquals(ref Emitter);
///Used as "null" UTF-32 character.
2013-05-30 22:23:23 +00:00
static immutable dcharNone = dchar.max;
///Emitter used to emit the scalar.
Emitter* emitter_;
///UTF-8 encoded text of the scalar to write.
string text_;
///Can we split the scalar into multiple lines?
bool split_;
///Are we currently going over spaces in the text?
bool spaces_;
///Are we currently going over line breaks in the text?
bool breaks_;
///Start and end byte of the text range we're currently working with.
size_t startByte_, endByte_;
///End byte of the text range including the currently processed character.
size_t nextEndByte_;
///Start and end character of the text range we're currently working with.
long startChar_, endChar_;
public:
///Construct a ScalarWriter using emitter to output text.
2012-09-13 23:16:05 +00:00
this(ref Emitter emitter, string text, const bool split = true) @trusted nothrow
{
emitter_ = &emitter;
text_ = text;
split_ = split;
}
///Destroy the ScalarWriter.
@trusted nothrow ~this()
{
text_ = null;
}
///Write text as single quoted scalar.
void writeSingleQuoted() @system
{
2012-09-13 23:16:05 +00:00
emitter_.writeIndicator("\'", Yes.needWhitespace);
spaces_ = breaks_ = false;
resetTextPosition();
do
{
const dchar c = nextChar();
if(spaces_)
{
if(c != ' ' && tooWide() && split_ &&
startByte_ != 0 && endByte_ != text_.length)
{
writeIndent(Flag!"ResetSpace".no);
updateRangeStart();
}
else if(c != ' ')
{
writeCurrentRange(Flag!"UpdateColumn".yes);
}
}
else if(breaks_)
{
if(!newlineSearch_.canFind(c))
{
writeStartLineBreak();
writeLineBreaks();
emitter_.writeIndent();
}
}
else if((c == dcharNone || c == '\'' || c == ' ' || newlineSearch_.canFind(c))
&& startChar_ < endChar_)
{
writeCurrentRange(Flag!"UpdateColumn".yes);
}
if(c == '\'')
{
emitter_.column_ += 2;
emitter_.writeString("\'\'");
startByte_ = endByte_ + 1;
startChar_ = endChar_ + 1;
}
updateBreaks(c, Flag!"UpdateSpaces".yes);
}while(endByte_ < text_.length);
2012-09-13 23:16:05 +00:00
emitter_.writeIndicator("\'", No.needWhitespace);
}
///Write text as double quoted scalar.
void writeDoubleQuoted() @system
{
resetTextPosition();
2012-09-13 23:16:05 +00:00
emitter_.writeIndicator("\"", Yes.needWhitespace);
do
{
const dchar c = nextChar();
//handle special characters
if(c == dcharNone || "\"\\\u0085\u2028\u2029\uFEFF"d.canFind(c) ||
!((c >= '\x20' && c <= '\x7E') ||
((c >= '\xA0' && c <= '\uD7FF') || (c >= '\uE000' && c <= '\uFFFD'))))
{
if(startChar_ < endChar_)
{
writeCurrentRange(Flag!"UpdateColumn".yes);
}
if(c != dcharNone)
{
auto appender = appender!string();
if((c in dyaml.escapes.toEscapes) !is null)
{
appender.put('\\');
appender.put(dyaml.escapes.toEscapes[c]);
}
else
{
//Write an escaped Unicode character.
const format = c <= 255 ? "\\x%02X":
c <= 65535 ? "\\u%04X": "\\U%08X";
formattedWrite(appender, format, cast(uint)c);
}
emitter_.column_ += appender.data.length;
emitter_.writeString(appender.data);
startChar_ = endChar_ + 1;
startByte_ = nextEndByte_;
}
}
if((endByte_ > 0 && endByte_ < text_.length - strideBack(text_, text_.length))
&& (c == ' ' || startChar_ >= endChar_)
&& (emitter_.column_ + endChar_ - startChar_ > emitter_.bestWidth_)
&& split_)
{
//text_[2:1] is ok in Python but not in D, so we have to use min()
emitter_.writeString(text_[min(startByte_, endByte_) .. endByte_]);
emitter_.writeString("\\");
emitter_.column_ += startChar_ - endChar_ + 1;
startChar_ = max(startChar_, endChar_);
startByte_ = max(startByte_, endByte_);
writeIndent(Flag!"ResetSpace".yes);
if(charAtStart() == ' ')
{
emitter_.writeString("\\");
++emitter_.column_;
}
}
}while(endByte_ < text_.length);
2012-09-13 23:16:05 +00:00
emitter_.writeIndicator("\"", No.needWhitespace);
}
///Write text as folded block scalar.
void writeFolded() @system
{
initBlock('>');
bool leadingSpace = true;
spaces_ = false;
breaks_ = true;
resetTextPosition();
do
{
const dchar c = nextChar();
if(breaks_)
{
if(!newlineSearch_.canFind(c))
{
if(!leadingSpace && c != dcharNone && c != ' ')
{
writeStartLineBreak();
}
leadingSpace = (c == ' ');
writeLineBreaks();
if(c != dcharNone){emitter_.writeIndent();}
}
}
else if(spaces_)
{
if(c != ' ' && tooWide())
{
writeIndent(Flag!"ResetSpace".no);
updateRangeStart();
}
else if(c != ' ')
{
writeCurrentRange(Flag!"UpdateColumn".yes);
}
}
else if(c == dcharNone || newlineSearch_.canFind(c) || c == ' ')
{
writeCurrentRange(Flag!"UpdateColumn".yes);
if(c == dcharNone){emitter_.writeLineBreak();}
}
updateBreaks(c, Flag!"UpdateSpaces".yes);
}while(endByte_ < text_.length);
}
///Write text as literal block scalar.
void writeLiteral() @system
{
initBlock('|');
breaks_ = true;
resetTextPosition();
do
{
const dchar c = nextChar();
if(breaks_)
{
if(!newlineSearch_.canFind(c))
{
writeLineBreaks();
if(c != dcharNone){emitter_.writeIndent();}
}
}
else if(c == dcharNone || newlineSearch_.canFind(c))
{
writeCurrentRange(Flag!"UpdateColumn".no);
if(c == dcharNone){emitter_.writeLineBreak();}
}
updateBreaks(c, Flag!"UpdateSpaces".no);
}while(endByte_ < text_.length);
}
///Write text as plain scalar.
void writePlain() @system
{
2012-09-13 23:16:05 +00:00
if(emitter_.context_ == Emitter.Context.Root){emitter_.openEnded_ = true;}
if(text_ == ""){return;}
if(!emitter_.whitespace_)
{
++emitter_.column_;
emitter_.writeString(" ");
}
emitter_.whitespace_ = emitter_.indentation_ = false;
spaces_ = breaks_ = false;
resetTextPosition();
do
{
const dchar c = nextChar();
if(spaces_)
{
if(c != ' ' && tooWide() && split_)
{
writeIndent(Flag!"ResetSpace".yes);
updateRangeStart();
}
else if(c != ' ')
{
writeCurrentRange(Flag!"UpdateColumn".yes);
}
}
else if(breaks_)
{
if(!newlineSearch_.canFind(c))
{
writeStartLineBreak();
writeLineBreaks();
writeIndent(Flag!"ResetSpace".yes);
}
}
else if(c == dcharNone || newlineSearch_.canFind(c) || c == ' ')
{
writeCurrentRange(Flag!"UpdateColumn".yes);
}
updateBreaks(c, Flag!"UpdateSpaces".yes);
}while(endByte_ < text_.length);
}
private:
///Get next character and move end of the text range to it.
2013-04-23 23:32:16 +00:00
@property dchar nextChar() pure @safe
{
++endChar_;
endByte_ = nextEndByte_;
if(endByte_ >= text_.length){return dcharNone;}
const c = text_[nextEndByte_];
//c is ascii, no need to decode.
if(c < 0x80)
{
++nextEndByte_;
return c;
}
return decode(text_, nextEndByte_);
}
///Get character at start of the text range.
2013-04-23 23:32:16 +00:00
@property dchar charAtStart() const pure @safe
{
size_t idx = startByte_;
return decode(text_, idx);
}
///Is the current line too wide?
2013-04-23 23:32:16 +00:00
@property bool tooWide() const pure @safe nothrow
{
return startChar_ + 1 == endChar_ &&
emitter_.column_ > emitter_.bestWidth_;
}
///Determine hints (indicators) for block scalar.
size_t determineBlockHints(char[] hints, uint bestIndent) const pure @trusted
{
size_t hintsIdx = 0;
if(text_.length == 0){return hintsIdx;}
dchar lastChar(const string str, ref size_t end)
{
size_t idx = end = end - strideBack(str, end);
return decode(text_, idx);
}
size_t end = text_.length;
const last = lastChar(text_, end);
const secondLast = end > 0 ? lastChar(text_, end) : 0;
if(newlineSearch_.canFind(text_[0]) || text_[0] == ' ')
{
hints[hintsIdx++] = cast(char)('0' + bestIndent);
}
if(!newlineSearch_.canFind(last))
{
hints[hintsIdx++] = '-';
}
else if(std.utf.count(text_) == 1 || newlineSearch_.canFind(secondLast))
{
hints[hintsIdx++] = '+';
}
return hintsIdx;
}
///Initialize for block scalar writing with specified indicator.
2012-09-13 23:16:05 +00:00
void initBlock(const char indicator) @system
{
char[4] hints;
hints[0] = indicator;
const hintsLength = 1 + determineBlockHints(hints[1 .. $], emitter_.bestIndent_);
2012-09-13 23:16:05 +00:00
emitter_.writeIndicator(cast(string)hints[0 .. hintsLength], Yes.needWhitespace);
if(hints.length > 0 && hints[$ - 1] == '+')
{
emitter_.openEnded_ = true;
}
emitter_.writeLineBreak();
}
///Write out the current text range.
2012-09-13 23:16:05 +00:00
void writeCurrentRange(const Flag!"UpdateColumn" updateColumn) @system
{
emitter_.writeString(text_[startByte_ .. endByte_]);
if(updateColumn){emitter_.column_ += endChar_ - startChar_;}
updateRangeStart();
}
///Write line breaks in the text range.
void writeLineBreaks() @system
{
foreach(const dchar br; text_[startByte_ .. endByte_])
{
if(br == '\n'){emitter_.writeLineBreak();}
else
{
char[4] brString;
const bytes = encode(brString, br);
emitter_.writeLineBreak(cast(string)brString[0 .. bytes]);
}
}
updateRangeStart();
}
///Write line break if start of the text range is a newline.
void writeStartLineBreak() @system
{
if(charAtStart == '\n'){emitter_.writeLineBreak();}
}
///Write indentation, optionally resetting whitespace/indentation flags.
2012-09-13 23:16:05 +00:00
void writeIndent(const Flag!"ResetSpace" resetSpace) @system
{
emitter_.writeIndent();
if(resetSpace)
{
emitter_.whitespace_ = emitter_.indentation_ = false;
}
}
///Move start of text range to its end.
void updateRangeStart() pure @safe nothrow
{
startByte_ = endByte_;
startChar_ = endChar_;
}
///Update the line breaks_ flag, optionally updating the spaces_ flag.
2012-09-13 23:16:05 +00:00
void updateBreaks(in dchar c, const Flag!"UpdateSpaces" updateSpaces) pure @trusted
{
if(c == dcharNone){return;}
breaks_ = newlineSearch_.canFind(c);
if(updateSpaces){spaces_ = c == ' ';}
}
///Move to the beginning of text.
void resetTextPosition() pure @safe nothrow
{
startByte_ = endByte_ = nextEndByte_ = 0;
startChar_ = endChar_ = -1;
}
}