diff --git a/include/yaml-cpp/emitter.h b/include/yaml-cpp/emitter.h index 8ff94d171..e79d5afe0 100644 --- a/include/yaml-cpp/emitter.h +++ b/include/yaml-cpp/emitter.h @@ -149,6 +149,13 @@ class YAML_CPP_API Emitter { private: std::unique_ptr 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 diff --git a/src/emitter.cpp b/src/emitter.cpp index bcae0bcd8..2308463a1 100644 --- a/src/emitter.cpp +++ b/src/emitter.cpp @@ -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(this)->m_stream << "\n"; + m_literalNeedsTrailingNewline = false; + } + return m_stream.str(); +} std::size_t Emitter::size() const { return m_stream.pos(); } @@ -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); @@ -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; } diff --git a/src/emitterutils.cpp b/src/emitterutils.cpp index 57f41ac38..3aeddcfad 100644 --- a/src/emitterutils.cpp +++ b/src/emitterutils.cpp @@ -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' @@ -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; diff --git a/src/emitterutils.h b/src/emitterutils.h index 328f782b4..23e160b8a 100644 --- a/src/emitterutils.h +++ b/src/emitterutils.h @@ -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, diff --git a/test/integration/emitter_test.cpp b/test/integration/emitter_test.cpp index 8dc3d6d26..c39757589 100644 --- a/test/integration/emitter_test.cpp +++ b/test/integration/emitter_test.cpp @@ -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(), "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(), "hello\n"); +} + TEST_F(EmitterTest, SingleQuotedWithCarriageReturn) { out << BeginMap; out << Key << "key" << Value << SingleQuoted << "a\rb";