Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/yaml-cpp/emitter.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ class YAML_CPP_API Emitter {
private:
std::unique_ptr<EmitterState> m_pState;
ostream_wrapper m_stream;

// Set when the last thing written was a literal block scalar whose
// chomping style ('strip' or 'keep') still owes the stream a trailing
// line break. Normally the next node emitted supplies that break as a
// side effect of starting its own line, but if nothing else is emitted,
// c_str() has to add it itself so the scalar round-trips correctly.
mutable bool m_literalNeedsTrailingNewline = false;
};

template <typename T>
Expand Down
16 changes: 14 additions & 2 deletions src/emitter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ Emitter::Emitter(std::ostream& stream)

Emitter::~Emitter() = default;

const char* Emitter::c_str() const { return m_stream.str(); }
const char* Emitter::c_str() const {
if (m_literalNeedsTrailingNewline) {
const_cast<Emitter*>(this)->m_stream << "\n";
m_literalNeedsTrailingNewline = false;
}
return m_stream.str();
}

std::size_t Emitter::size() const { return m_stream.pos(); }

Expand Down Expand Up @@ -282,6 +288,11 @@ void Emitter::EmitNewline() {
// Put the stream in a state so we can simply write the next node
// E.g., if we're in a sequence, write the "- "
void Emitter::PrepareNode(EmitterNodeType::value child) {
// Whatever we're about to emit starts its own line (or continues the
// current one), so any line break a previous literal block scalar was
// relying on us to provide is no longer something c_str() needs to add.
m_literalNeedsTrailingNewline = false;

switch (m_pState->CurGroupNodeType()) {
case EmitterNodeType::NoType:
PrepareTopNode(child);
Expand Down Expand Up @@ -750,7 +761,8 @@ Emitter& Emitter::Write(const char* str, std::size_t size) {
break;
case StringFormat::Literal:
Utils::WriteLiteralString(m_stream, str, size,
m_pState->CurIndent() + m_pState->GetIndent());
m_pState->CurIndent() + m_pState->GetIndent(),
&m_literalNeedsTrailingNewline);
break;
}

Expand Down
21 changes: 16 additions & 5 deletions src/emitterutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ bool WriteDoubleQuotedString(ostream_wrapper& out, const char* str, std::size_t
}

bool WriteLiteralString(ostream_wrapper& out, const char* str, std::size_t size,
std::size_t indent) {
std::size_t indent, bool* needs_trailing_newline) {
// depending on the numbers of new lines at the end of the string
// we need to use 'clip (-)', 'strip (default)' or 'keep' (+) annotation.
// if there is no newline at the end, we need 'clip'
Expand All @@ -405,18 +405,29 @@ bool WriteLiteralString(ostream_wrapper& out, const char* str, std::size_t size,
// see YAML spec 1.2 chapter '8.1.1.2 Block Chomping Indicator' for more information
// https://yaml.org/spec/1.2.2/#8112-block-chomping-indicator


// The chomping depends on the number of new lines.
// The output following this 'WriteLiteralString' call will add an additional '\n',
// because of this we need to remove one in the case of 'strip' and 'keep'.
if (needs_trailing_newline)
*needs_trailing_newline = false;

// The chomping depends on the number of new lines. Whatever is emitted
// after this call normally starts with the '\n' that separates it from
// this node, and that's the one we rely on for the final line break of
// the 'strip' and 'keep' styles below, so we drop one from what we write
// here. But when this literal happens to be the very last thing emitted
// (no sibling or closing node follows it), that '\n' never materializes
// and the caller needs to know a line break is still owed so it can add
// one itself once the document is finalized.
if (size == 0 || str[size-1] != '\n') { // clip
out << "|-\n";
} else if (size == 1 || str[size-2] != '\n') { // strip
out << "|\n";
size -= 1;
if (needs_trailing_newline)
*needs_trailing_newline = true;
} else { // 'keep'
out << "|+\n";
size -= 1;
if (needs_trailing_newline)
*needs_trailing_newline = true;
}
int codePoint;
for (const char* i = str;
Expand Down
3 changes: 2 additions & 1 deletion src/emitterutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ bool WriteSingleQuotedString(ostream_wrapper& out, const char* str, std::size_t
bool WriteDoubleQuotedString(ostream_wrapper& out, const char* str, std::size_t size,
StringEscaping::value stringEscaping);
bool WriteLiteralString(ostream_wrapper& out, const char* str, std::size_t size,
std::size_t indent);
std::size_t indent,
bool* needs_trailing_newline = nullptr);
bool WriteChar(ostream_wrapper& out, char ch,
StringEscaping::value stringEscapingStyle);
bool WriteComment(ostream_wrapper& out, const char* str, std::size_t size,
Expand Down
23 changes: 23 additions & 0 deletions test/integration/emitter_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,29 @@ TEST_F(EmitterTest, LiteralWithAndWithoutTrailingEmptyLines) {
"- something");
}

TEST_F(EmitterTest, LiteralTrailingNewlinesRoundTripAsLastMapValue) {
// A literal scalar using the 'strip' or 'keep' chomping style used to
// lose a trailing newline whenever it was the very last thing written,
// because the emitter counted on whatever came after it to supply one
// more '\n'. With nothing following, that line break was never written.
out << BeginMap;
out << Key << "data" << Value << Literal << "hello\nworld\n\n";
out << EndMap;

Node reparsed = Load(out.c_str());
EXPECT_EQ(reparsed["data"].as<std::string>(), "hello\nworld\n\n");
}

TEST_F(EmitterTest, LiteralTrailingNewlinesRoundTripAsLastSeqEntry) {
out << BeginSeq;
out << "before";
out << Literal << "hello\n";
out << EndSeq;

Node reparsed = Load(out.c_str());
EXPECT_EQ(reparsed[1].as<std::string>(), "hello\n");
}

TEST_F(EmitterTest, SingleQuotedWithCarriageReturn) {
out << BeginMap;
out << Key << "key" << Value << SingleQuoted << "a\rb";
Expand Down