From 2e7cda1dc92bc51f9a8cf1b1258d6b87566f891f Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Thu, 24 Sep 2026 14:21:34 -0300 Subject: [PATCH 1/6] WIP Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 2 +- .../core/yaml/include/sourcemeta/core/yaml.h | 39 ++ .../include/sourcemeta/core/yaml_roundtrip.h | 27 + vendor/core/src/core/yaml/lexer.h | 100 ++-- vendor/core/src/core/yaml/parser.h | 267 ++++++++-- vendor/core/src/core/yaml/stringify.h | 493 ++++++++++++++---- vendor/core/src/core/yaml/yaml.cc | 70 ++- 7 files changed, 784 insertions(+), 214 deletions(-) diff --git a/DEPENDENCIES b/DEPENDENCIES index f4ef60d76..1caa1fe92 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core f503133930d61dabbd59cded7e432a0addb11625 +core https://github.com/sourcemeta/core 400cfc560cf1cab55734fa356f5c9101fbce34a9 jsonbinpack https://github.com/sourcemeta/jsonbinpack ad91fc9e1129b5ee71983fdb709e3340632f6fbd blaze https://github.com/sourcemeta/blaze e680badab795e9cdf30752f7dcfe77be708e05d7 ctrf https://github.com/ctrf-io/ctrf 93ea827d951390190171d37443bff169cf47c808 diff --git a/vendor/core/src/core/yaml/include/sourcemeta/core/yaml.h b/vendor/core/src/core/yaml/include/sourcemeta/core/yaml.h index e9b614bb6..eb45c4c03 100644 --- a/vendor/core/src/core/yaml/include/sourcemeta/core/yaml.h +++ b/vendor/core/src/core/yaml/include/sourcemeta/core/yaml.h @@ -170,6 +170,31 @@ auto read_yaml_or_json(const std::filesystem::path &path, JSON &output, SOURCEMETA_CORE_YAML_EXPORT auto parse_yaml(const JSON::String &input, YAMLRoundTrip &roundtrip) -> JSON; +/// @ingroup yaml +/// +/// Create a JSON document from a C++ standard input stream that represents a +/// YAML document, collecting round-trip metadata to reproduce the original +/// formatting. The stream is left just after the document that was read, so +/// that a stream holding several documents can be read one document at a +/// time. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// #include +/// +/// std::istringstream stream{"hello: world\n---\nsecond: document\n"}; +/// while (stream.peek() != std::char_traits::eof()) { +/// sourcemeta::core::YAMLRoundTrip roundtrip; +/// const sourcemeta::core::JSON document = +/// sourcemeta::core::parse_yaml(stream, roundtrip); +/// } +/// ``` +SOURCEMETA_CORE_YAML_EXPORT +auto parse_yaml(std::basic_istream &stream, + YAMLRoundTrip &roundtrip) -> JSON; + /// @ingroup yaml /// /// Parse a YAML string with round-trip metadata into an existing JSON value, @@ -181,6 +206,20 @@ SOURCEMETA_CORE_YAML_EXPORT auto parse_yaml(const JSON::String &input, YAMLRoundTrip &roundtrip, JSON &output, const JSON::ParseCallback &callback) -> void; +/// @ingroup yaml +/// +/// Parse a YAML document from a C++ standard input stream with round-trip +/// metadata into an existing JSON value, invoking the given callback during +/// parsing. The stream is left just after the document that was read, so that +/// a stream holding several documents can be read one document at a time. The +/// result is constructed directly into the given reference rather than +/// returned by value to ensure that references passed through the parse +/// callback remain valid after parsing completes. +SOURCEMETA_CORE_YAML_EXPORT +auto parse_yaml(std::basic_istream &stream, + YAMLRoundTrip &roundtrip, JSON &output, + const JSON::ParseCallback &callback) -> void; + /// @ingroup yaml /// /// Stringify a JSON document as YAML, using round-trip metadata collected diff --git a/vendor/core/src/core/yaml/include/sourcemeta/core/yaml_roundtrip.h b/vendor/core/src/core/yaml/include/sourcemeta/core/yaml_roundtrip.h index 9a655dfa1..874288dd8 100644 --- a/vendor/core/src/core/yaml/include/sourcemeta/core/yaml_roundtrip.h +++ b/vendor/core/src/core/yaml/include/sourcemeta/core/yaml_roundtrip.h @@ -84,6 +84,13 @@ class SOURCEMETA_CORE_YAML_EXPORT YAMLRoundTrip { std::optional content_value; /// The anchor name attached to the node std::optional anchor; + /// The tag attached to the node, as it was written + std::optional tag; + /// The type the tagged node held when the tag was recorded, so that a tag + /// is only reproduced while the document still holds that kind of value + std::optional tag_type; + /// Whether the tag precedes the anchor + bool tag_before_anchor{false}; /// The comments preceding the node std::vector comments_before; /// The comment on the same line as the node @@ -92,6 +99,16 @@ class SOURCEMETA_CORE_YAML_EXPORT YAMLRoundTrip { std::optional comment_on_indicator; /// Whether the flow collection uses compact formatting bool compact_flow{false}; + /// Whether the flow collection is padded with a space inside its delimiters + bool padded_flow{false}; + /// Whether a block sequence that is the value of a mapping key sits at the + /// same indentation as that key rather than one level in. + /// See https://yaml.org/spec/1.2.2/#821-block-sequences + bool unindented_sequence{false}; + /// How many items the sequence held when it was read, so that comments and + /// node properties recorded against a position are only reproduced while + /// that position still holds the item they were read from + std::optional sequence_size; }; /// The recorded formatting for each node by pointer @@ -102,6 +119,11 @@ class SOURCEMETA_CORE_YAML_EXPORT YAMLRoundTrip { std::unordered_map key_styles; /// The original quoted content for each mapping key std::unordered_map key_quoted_contents; + /// The directive and comment lines that precede the document start marker, + /// in the order they were written. A document that carries a directive + /// always begins with an explicit start marker. + /// See https://yaml.org/spec/1.2.2/#912-document-markers + std::vector document_prefix; /// Whether the document begins with an explicit start marker bool explicit_document_start{false}; /// Whether the document ends with an explicit end marker @@ -120,6 +142,11 @@ class SOURCEMETA_CORE_YAML_EXPORT YAMLRoundTrip { std::vector trailing_comments; /// The indentation width used when emitting the document std::size_t indent_width{2}; + /// Whether the document begins with a byte order mark + bool byte_order_mark{false}; + /// Whether the document separates its lines with a carriage return and a + /// line feed rather than a line feed alone + bool carriage_returns{false}; }; #if defined(_MSC_VER) diff --git a/vendor/core/src/core/yaml/lexer.h b/vendor/core/src/core/yaml/lexer.h index 049440ff7..f42fbafe6 100644 --- a/vendor/core/src/core/yaml/lexer.h +++ b/vendor/core/src/core/yaml/lexer.h @@ -74,6 +74,14 @@ class Lexer { this->validate_characters(); } + // A carriage return is a line break rather than comment content, so it never + // belongs to the text of a comment that a carriage return ends. + // See https://yaml.org/spec/1.2.2/#66-comments + static auto comment_text(const std::string_view raw) -> std::string { + return std::string{raw.ends_with('\r') ? raw.substr(0, raw.size() - 1) + : raw}; + } + // The number of leading bytes consumed by a stripped byte order mark, so a // caller reading from a stream can map a consumed count back to the original // input offset @@ -344,6 +352,26 @@ class Lexer { return this->position_; } + // The line break that closes a document suffix belongs to the document that + // is ending rather than to whatever follows it. + // See https://yaml.org/spec/1.2.2/#912-document-markers + auto skip_line_break() -> void { + if (this->position_ >= this->input_.size()) { + return; + } + + const auto current{this->input_[this->position_]}; + if (current != '\n' && current != '\r') { + return; + } + + this->advance(1); + if (current == '\r' && this->position_ < this->input_.size() && + this->input_[this->position_] == '\n') { + this->advance(1); + } + } + auto take_inline_comment() -> std::optional { auto result{std::move(this->inline_comment_buffer_)}; this->inline_comment_buffer_.reset(); @@ -443,7 +471,13 @@ class Lexer { if (this->position_ >= this->input_.size()) { break; } - if (this->input_[this->position_] == '\n') { + // A lone carriage return is a line break of its own, while the one that + // opens a carriage return and line feed pair leaves the counting to the + // line feed. See https://yaml.org/spec/1.2.2/#54-line-break-characters + const auto character{this->input_[this->position_]}; + if (character == '\n' || + (character == '\r' && (this->position_ + 1 >= this->input_.size() || + this->input_[this->position_ + 1] != '\n'))) { this->line_++; this->column_ = 1; } else { @@ -537,8 +571,8 @@ class Lexer { this->advance(1); } if (this->roundtrip_) { - std::string text{this->input_.substr( - comment_start, this->position_ - comment_start)}; + std::string text{comment_text(this->input_.substr( + comment_start, this->position_ - comment_start))}; if (comment_line == this->comment_reference_line_ && this->comment_reference_line_ > 0 && !this->inline_comment_buffer_.has_value()) { @@ -1090,59 +1124,20 @@ class Lexer { return codepoint_to_utf8(character); } - [[nodiscard]] auto calculate_parent_indentation( - const std::size_t indicator_position) const noexcept -> std::size_t { - std::size_t line_start{indicator_position}; - while (line_start > 0 && this->input_[line_start - 1] != '\n' && - this->input_[line_start - 1] != '\r') { - line_start--; - } - - std::size_t leading_spaces{0}; - std::size_t scan_position{line_start}; - while (scan_position < this->input_.size() && - this->input_[scan_position] == ' ') { - leading_spaces++; - scan_position++; - } - - bool in_sequence_entry{false}; - if (scan_position < this->input_.size() - 1 && - this->input_[scan_position] == '-' && - this->input_[scan_position + 1] == ' ') { - in_sequence_entry = true; - } - - bool is_mapping_value_same_line{false}; - for (std::size_t index = line_start; index < indicator_position; ++index) { - if (this->input_[index] == ':') { - is_mapping_value_same_line = true; - break; - } - } - - if (in_sequence_entry && is_mapping_value_same_line) { - return leading_spaces + 2; - } - - if (is_mapping_value_same_line) { - return leading_spaces; - } - - return 0; - } - auto detect_block_scalar_indent(const std::size_t explicit_indent, - const std::size_t indicator_position, const std::uint64_t start_line, const std::uint64_t start_column) -> std::size_t { std::size_t content_indent{0}; if (explicit_indent > 0) { - const auto parent_indent{ - this->calculate_parent_indentation(indicator_position)}; - content_indent = parent_indent + explicit_indent; + // The content indentation level of a block scalar is the indentation + // level of the node itself plus the indicator, and the node at the + // document root sits one level further out than the leftmost column. + // See https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator + content_indent = this->block_indent_ == SIZE_MAX + ? explicit_indent - 1 + : this->block_indent_ + explicit_indent; } else { const auto saved_position{this->position_}; const auto saved_line{this->line_}; @@ -1196,7 +1191,6 @@ class Lexer { auto scan_block_scalar(const ScalarStyle style) -> Token { const auto start_line{this->line_}; const auto start_column{this->column_}; - const auto indicator_position{this->position_}; this->advance(1); @@ -1228,8 +1222,8 @@ class Lexer { this->advance(1); } if (this->roundtrip_) { - this->block_scalar_comment_ = std::string{this->input_.substr( - comment_start, this->position_ - comment_start)}; + this->block_scalar_comment_ = comment_text(this->input_.substr( + comment_start, this->position_ - comment_start)); } } else if (current == '\n' || current == '\r') { break; @@ -1260,7 +1254,7 @@ class Lexer { } const auto content_indent{this->detect_block_scalar_indent( - explicit_indent, indicator_position, start_line, start_column)}; + explicit_indent, start_line, start_column)}; std::size_t blank_line_count{0}; bool previous_was_more_indented{false}; diff --git a/vendor/core/src/core/yaml/parser.h b/vendor/core/src/core/yaml/parser.h index ffef25ffe..a42285dec 100644 --- a/vendor/core/src/core/yaml/parser.h +++ b/vendor/core/src/core/yaml/parser.h @@ -9,7 +9,7 @@ #include #include -#include // std::max +#include // std::max, std::find_if #include // assert #include // std::uint64_t, std::int64_t #include // std::optional @@ -46,6 +46,7 @@ class Parser { : lexer_{lexer}, callback_{callback}, roundtrip_{roundtrip} {} auto parse() -> JSON { + bool empty_document{false}; std::optional token; if (!this->pending_tokens_.empty()) { @@ -72,7 +73,7 @@ class Parser { if (token->type == TokenType::DirectiveYAML || token->type == TokenType::DirectiveTag || token->type == TokenType::DirectiveReserved) { - this->process_directives(token.value()); + this->process_directives(token.value(), true); } if (token->type == TokenType::DocumentStart) { @@ -82,6 +83,7 @@ class Parser { this->roundtrip_->explicit_document_start = true; } this->document_start_line_ = token->line; + this->lexer_->skip_line_break(); const auto pos_before_next{this->lexer_->position()}; token = this->lexer_->next(); if (this->roundtrip_ != nullptr) { @@ -95,8 +97,17 @@ class Parser { if (token.has_value() && token->type == TokenType::DocumentStart) { this->pending_tokens_.push_back(token.value()); this->pending_token_position_ = pos_before_next; + return JSON{nullptr}; } - return JSON{nullptr}; + + if (this->roundtrip_ != nullptr) { + this->roundtrip_->post_start_comments = + this->lexer_->take_preceding_comments(); + } + + // A document with no node of its own still ends the way any other + // does, so what follows it is read the same way + empty_document = true; } } else if (!token.has_value() || token->type == TokenType::StreamEnd) [[unlikely]] { @@ -114,26 +125,33 @@ class Parser { return JSON{nullptr}; } - if (this->roundtrip_ != nullptr) { - auto comments{this->lexer_->take_preceding_comments()}; - this->lexer_->take_inline_comment(); - if (this->roundtrip_->explicit_document_start) { - this->roundtrip_->post_start_comments = std::move(comments); - } else { - this->roundtrip_->leading_comments = std::move(comments); + JSON result{nullptr}; + auto pos_before_token{this->lexer_->position()}; + + if (!empty_document) { + if (this->roundtrip_ != nullptr) { + auto comments{this->lexer_->take_preceding_comments()}; + this->lexer_->take_inline_comment(); + if (this->roundtrip_->explicit_document_start) { + this->roundtrip_->post_start_comments = std::move(comments); + } else { + this->roundtrip_->leading_comments = std::move(comments); + } } - } - auto result{this->parse_value(token.value(), JSON::ParseContext::Root, 0, - EMPTY_PROPERTY)}; + result = this->parse_value(token.value(), JSON::ParseContext::Root, 0, + EMPTY_PROPERTY); - auto pos_before_token{this->lexer_->position()}; - token = this->next_token(); - if (this->roundtrip_ != nullptr) { - auto root_inline{this->lexer_->take_inline_comment()}; - if (root_inline.has_value()) { - this->roundtrip_->styles[this->pointer_stack_].comment_inline = - std::move(root_inline); + this->attach_leading_comments_to_first_key(result); + + pos_before_token = this->lexer_->position(); + token = this->next_token(); + if (this->roundtrip_ != nullptr) { + auto root_inline{this->lexer_->take_inline_comment()}; + if (root_inline.has_value()) { + this->roundtrip_->styles[this->pointer_stack_].comment_inline = + std::move(root_inline); + } } } while (token.has_value() && token->type == TokenType::DocumentEnd) { @@ -143,6 +161,7 @@ class Parser { this->lexer_->take_preceding_comments(); this->roundtrip_->explicit_document_end = true; } + this->lexer_->skip_line_break(); pos_before_token = this->lexer_->position(); token = this->next_token(); if (this->roundtrip_ != nullptr) { @@ -153,7 +172,9 @@ class Parser { if (this->roundtrip_ != nullptr) { auto trailing{this->lexer_->take_preceding_comments()}; - if (!trailing.empty()) { + const bool ends_the_stream{!token.has_value() || + token->type == TokenType::StreamEnd}; + if (!trailing.empty() && ends_the_stream) { this->roundtrip_->trailing_comments = std::move(trailing); } } @@ -296,11 +317,59 @@ class Parser { return total; } - auto process_directives(Token &token) -> void { + // A comment block that runs straight into the first key of the document reads + // as belonging to that key, so it is recorded there and travels with the key + // if the document is later rearranged. A blank line in between instead marks + // the block as a header for the document as a whole + auto attach_leading_comments_to_first_key(const JSON &result) -> void { + if ((this->roundtrip_ == nullptr) || !result.is_object() || + result.empty()) { + return; + } + + auto &comments{this->roundtrip_->explicit_document_start + ? this->roundtrip_->post_start_comments + : this->roundtrip_->leading_comments}; + const auto blank{ + std::find_if(comments.crbegin(), comments.crend(), + [](const auto &comment) { return comment.empty(); })}; + if (blank == comments.crbegin()) { + return; + } + + const auto first{blank.base()}; + Pointer pointer{result.as_object().cbegin()->first}; + auto &attached{this->roundtrip_->styles[pointer].comments_before}; + attached.insert(attached.cbegin(), first, comments.cend()); + comments.erase(first, comments.cend()); + } + + // A flow collection may be written with a space just inside its delimiters, + // which the first token after the opening one gives away + auto record_flow_padding(const Token &start_token, + const std::optional &first) -> void { + if ((this->roundtrip_ == nullptr) || !first.has_value() || + first->line != start_token.line || + first->column <= start_token.column + 1) { + return; + } + + this->roundtrip_->styles[this->pointer_stack_].padded_flow = true; + } + + auto process_directives(Token &token, const bool record = false) -> void { bool seen_yaml_directive{false}; while (token.type == TokenType::DirectiveYAML || token.type == TokenType::DirectiveTag || token.type == TokenType::DirectiveReserved) { + if (record && this->roundtrip_ != nullptr) { + auto &prefix{this->roundtrip_->document_prefix}; + for (auto &comment : this->lexer_->take_preceding_comments()) { + prefix.push_back(std::move(comment)); + } + + prefix.emplace_back(token.value); + } if (token.type == TokenType::DirectiveYAML) { if (seen_yaml_directive) [[unlikely]] { throw YAMLParseError{token.line, token.column, @@ -535,6 +604,8 @@ class Parser { std::optional anchor_name; std::uint64_t anchor_line{0}; std::optional tag; + std::optional raw_tag; + bool tag_before_anchor{false}; std::size_t anchor_count{0}; std::optional anchor_inline_comment; Token current_token{token}; @@ -563,11 +634,24 @@ class Parser { anchor_count++; } else { tag = this->resolve_tag(current_token.value); + if (this->roundtrip_ != nullptr) { + raw_tag = std::string{current_token.value}; + tag_before_anchor = anchor_count == 0; + } } auto next{this->lexer_->next()}; if ((this->roundtrip_ != nullptr) && anchor_name.has_value()) { anchor_inline_comment = this->lexer_->take_inline_comment(); + } else if ((this->roundtrip_ != nullptr) && + context == JSON::ParseContext::Root && + this->document_start_line_ > 0 && + current_token.line == this->document_start_line_ && + !this->roundtrip_->document_start_comment.has_value()) { + // A comment that trails the node properties of the root node still + // sits on the document start marker line, which is where it is written + this->roundtrip_->document_start_comment = + this->lexer_->take_inline_comment(); } if (!next.has_value() || next->type == TokenType::StreamEnd || next->type == TokenType::DocumentEnd || @@ -588,6 +672,7 @@ class Parser { style.comment_inline = std::move(anchor_inline_comment); } } + this->record_tag(raw_tag, tag_before_anchor, empty_value); if ((this->roundtrip_ != nullptr) && context != JSON::ParseContext::Root) { this->pointer_stack_.pop_back(); @@ -602,23 +687,29 @@ class Parser { if (after.has_value() && after->type == TokenType::BlockMappingValue) { this->pending_tokens_.push_back(current_token); this->pending_tokens_.push_back(after.value()); + JSON empty_value{nullptr}; + if (tag.has_value() && tag.value() == "tag:yaml.org,2002:str") { + empty_value = JSON{std::string{}}; + } if (anchor_name.has_value()) { this->register_anchored_null(anchor_name.value(), token, context, index, property, anchor_inline_comment); } + this->record_tag(raw_tag, tag_before_anchor, empty_value); if ((this->roundtrip_ != nullptr) && context != JSON::ParseContext::Root) { this->pointer_stack_.pop_back(); } - return JSON{nullptr}; + return empty_value; } if (after.has_value()) { this->pending_tokens_.push_back(after.value()); } } - if (anchor_name.has_value() && context == JSON::ParseContext::Index && + if ((anchor_name.has_value() || tag.has_value()) && + context == JSON::ParseContext::Index && current_token.type == TokenType::BlockSequenceEntry) { const auto block_indent{this->lexer_->block_indent()}; const auto entry_indent{ @@ -627,13 +718,21 @@ class Parser { : 0UZ}; if (block_indent != SIZE_MAX && entry_indent <= block_indent) { this->pending_tokens_.push_back(current_token); - this->register_anchored_null(anchor_name.value(), token, context, - index, property, anchor_inline_comment); + JSON empty_value{nullptr}; + if (tag.has_value() && tag.value() == "tag:yaml.org,2002:str") { + empty_value = JSON{std::string{}}; + } + if (anchor_name.has_value()) { + this->register_anchored_null(anchor_name.value(), token, context, + index, property, + anchor_inline_comment); + } + this->record_tag(raw_tag, tag_before_anchor, empty_value); if ((this->roundtrip_ != nullptr) && context != JSON::ParseContext::Root) { this->pointer_stack_.pop_back(); } - return JSON{nullptr}; + return empty_value; } } } @@ -646,6 +745,7 @@ class Parser { empty_value = JSON{std::string{}}; } this->pending_tokens_.push_back(current_token); + this->record_tag(raw_tag, tag_before_anchor, empty_value); if ((this->roundtrip_ != nullptr) && context != JSON::ParseContext::Root) { this->pointer_stack_.pop_back(); @@ -812,6 +912,13 @@ class Parser { } } + this->record_tag(raw_tag, tag_before_anchor, result); + + if ((this->roundtrip_ != nullptr) && result.is_array()) { + this->roundtrip_->styles[this->pointer_stack_].sequence_size = + result.size(); + } + if ((this->roundtrip_ != nullptr) && context != JSON::ParseContext::Root) { this->pointer_stack_.pop_back(); } @@ -1090,6 +1197,7 @@ class Parser { bool found_compact_separator{false}; auto token{this->next_token()}; + this->record_flow_padding(start_token, token); while (token.has_value() && token->type != TokenType::MappingEnd) { if (token->type == TokenType::FlowEntry) { @@ -1224,6 +1332,7 @@ class Parser { bool found_compact_separator{false}; auto token{this->next_token()}; + this->record_flow_padding(start_token, token); std::size_t element_index{0}; while (token.has_value() && token->type != TokenType::SequenceEnd) { @@ -1345,6 +1454,11 @@ class Parser { const auto sequence_indent{ base_column > 0 ? static_cast(base_column - 1) : 0UZ}; this->detect_indent_width(key_column, base_column); + if ((this->roundtrip_ != nullptr) && + context == JSON::ParseContext::Property && key_column > 0 && + base_column == key_column) { + this->roundtrip_->styles[this->pointer_stack_].unindented_sequence = true; + } this->lexer_->set_block_indent(sequence_indent); this->record_preceding_comments_for_index(0); @@ -1699,6 +1813,37 @@ class Parser { return anchored.value; } + // A node that stands on a later line than the key it belongs to has to be + // indented past the mapping for that mapping to own it. A block sequence is + // the one exception, as it may sit at the very indentation of its key. + // See https://yaml.org/spec/1.2.2/#821-block-sequences + [[nodiscard]] auto starts_mapping_value(const Token &token, + const std::uint64_t key_line, + const std::uint64_t base_column) const + -> bool { + if (token.line == key_line) { + return true; + } + + return token.type == TokenType::BlockSequenceEntry + ? token.column >= base_column + : token.column > base_column; + } + + // A node property that decorates a block node rather than a key of the + // mapping it sits in has to be indented past that mapping, so one that opens + // a block sequence from the mapping's own indentation has nowhere to belong. + // See https://yaml.org/spec/1.2.2/#822-block-mappings + auto reject_misplaced_property(const Token &property, + const std::optional &node) const + -> void { + if (node.has_value() && node->type == TokenType::BlockSequenceEntry) + [[unlikely]] { + throw YAMLParseError{property.line, property.column, + "Node property at wrong indentation level"}; + } + } + auto next_token() -> std::optional { std::optional result; if (!this->pending_tokens_.empty()) { @@ -1754,7 +1899,7 @@ class Parser { next->type == TokenType::StreamEnd || next->type == TokenType::DocumentEnd) { if (next.has_value() && next->type == TokenType::Scalar && - (next->line == key_line || next->column != base_column)) { + this->starts_mapping_value(next.value(), key_line, base_column)) { this->record_inline_comment_for_key(key, next->line != key_line); auto value{this->parse_value(next.value(), JSON::ParseContext::Property, 0, key, key_line, key_column)}; @@ -1779,18 +1924,22 @@ class Parser { next->type == TokenType::BlockSequenceEntry || next->type == TokenType::Anchor || next->type == TokenType::Tag || next->type == TokenType::Alias) { - if (next->type == TokenType::BlockSequenceEntry && next->line == key_line) - [[unlikely]] { - throw YAMLParseError{ - next->line, next->column, - "Block sequence entry on same line as mapping key"}; - } - this->record_inline_comment_for_key(key, next->line != key_line); - auto value{this->parse_value(next.value(), JSON::ParseContext::Property, - 0, key, key_line, key_column)}; - result.assign(key, std::move(value)); - next = this->next_token(); - this->record_inline_comment_for_key(key); + if (!this->starts_mapping_value(next.value(), key_line, base_column)) { + result.assign(key, JSON{nullptr}); + } else { + if (next->type == TokenType::BlockSequenceEntry && + next->line == key_line) [[unlikely]] { + throw YAMLParseError{ + next->line, next->column, + "Block sequence entry on same line as mapping key"}; + } + this->record_inline_comment_for_key(key, next->line != key_line); + auto value{this->parse_value(next.value(), JSON::ParseContext::Property, + 0, key, key_line, key_column)}; + result.assign(key, std::move(value)); + next = this->next_token(); + this->record_inline_comment_for_key(key); + } } else { result.assign(key, JSON{nullptr}); } @@ -1901,16 +2050,27 @@ class Parser { auto effective_column{next->column}; std::optional subsequent_key_tag; + // A node property introduces the key it decorates, so a mapping that + // does not reach that key must leave the property alone as well + if ((next->type == TokenType::Anchor || next->type == TokenType::Tag) && + effective_column != base_column) { + break; + } + if (next->type == TokenType::Anchor) { + const auto property_token{next.value()}; next = this->next_token(); + this->reject_misplaced_property(property_token, next); if (!next.has_value() || next->type != TokenType::Scalar) { continue; } } if (next->type == TokenType::Tag) { + const auto property_token{next.value()}; subsequent_key_tag = this->resolve_tag(next->value); next = this->next_token(); + this->reject_misplaced_property(property_token, next); if (!next.has_value() || next->type != TokenType::Scalar) { continue; } @@ -1948,7 +2108,8 @@ class Parser { next = this->next_token(); if (!next.has_value() || next->type == TokenType::Scalar) { - if (next.has_value()) { + if (next.has_value() && + this->starts_mapping_value(next.value(), key_line, base_column)) { auto value{this->parse_value(next.value(), JSON::ParseContext::Property, 0, key, key_line, key_column)}; @@ -1962,12 +2123,15 @@ class Parser { next->type == TokenType::DocumentStart) { result.assign(key, JSON{nullptr}); break; - } else { + } else if (this->starts_mapping_value(next.value(), key_line, + base_column)) { auto value{this->parse_value(next.value(), JSON::ParseContext::Property, 0, key, key_line, key_column)}; result.assign(key, std::move(value)); next = this->next_token(); + } else { + result.assign(key, JSON{nullptr}); } continue; } @@ -2006,7 +2170,7 @@ class Parser { if (!next.has_value() || next->type == TokenType::Scalar) { if (next.has_value() && - (next->line == key_line || next->column != base_column)) { + this->starts_mapping_value(next.value(), key_line, base_column)) { this->record_inline_comment_for_key(key, next->line != key_line); auto after{this->next_token()}; if (after.has_value()) { @@ -2028,12 +2192,15 @@ class Parser { next->type == TokenType::DocumentStart) { result.assign(key, JSON{nullptr}); break; - } else { + } else if (this->starts_mapping_value(next.value(), key_line, + base_column)) { this->record_inline_comment_for_key(key, next->line != key_line); auto value{this->parse_value(next.value(), JSON::ParseContext::Property, 0, key, key_line, key_column)}; result.assign(key, std::move(value)); next = this->next_token(); + } else { + result.assign(key, JSON{nullptr}); } } @@ -2164,6 +2331,18 @@ class Parser { this->roundtrip_->styles[this->pointer_stack_].collection = style; } + auto record_tag(const std::optional &raw_tag, + const bool tag_before_anchor, const JSON &value) -> void { + if ((this->roundtrip_ == nullptr) || !raw_tag.has_value()) { + return; + } + + auto &node_style{this->roundtrip_->styles[this->pointer_stack_]}; + node_style.tag = raw_tag.value(); + node_style.tag_type = value.type(); + node_style.tag_before_anchor = tag_before_anchor; + } + auto record_scalar_style(const Token &token, const JSON &value) -> void { if (this->roundtrip_ == nullptr) { return; diff --git a/vendor/core/src/core/yaml/stringify.h b/vendor/core/src/core/yaml/stringify.h index 2f70421ef..a96d309e7 100644 --- a/vendor/core/src/core/yaml/stringify.h +++ b/vendor/core/src/core/yaml/stringify.h @@ -26,13 +26,27 @@ static constexpr std::array HEX_DIGITS{{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}}; -inline auto write_indent(OutputStream &stream, const std::size_t indent, - const std::size_t width = INDENT_WIDTH) -> void { - for (std::size_t index{0}; index < indent * width; ++index) { +// A processor is free to write line breaks with whatever convention suits the +// document. See https://yaml.org/spec/1.2.2/#54-line-break-characters +inline auto write_break(OutputStream &stream, const YAMLRoundTrip *roundtrip) + -> void { + if ((roundtrip != nullptr) && roundtrip->carriage_returns) { + stream.put('\r'); + } + + stream.put('\n'); +} + +inline auto write_indent(OutputStream &stream, const std::size_t columns) + -> void { + for (std::size_t index{0}; index < columns; ++index) { stream.put(' '); } } +// The width of the "- " indicator that opens a block sequence entry +static constexpr std::size_t SEQUENCE_INDICATOR_WIDTH{2}; + inline auto looks_like_number(const std::string &value) -> bool { std::size_t start{0}; if (value[0] == '-' || value[0] == '+') { @@ -214,49 +228,48 @@ inline auto write_string(OutputStream &stream, const std::string &value) } inline auto write_block_scalar( - OutputStream &stream, const std::string &value, const std::size_t indent, - const YAMLRoundTrip::ScalarStyle style, + OutputStream &stream, const std::string &value, + const std::size_t content_columns, const YAMLRoundTrip::ScalarStyle style, const YAMLRoundTrip::Chomping chomping, const std::optional &header_comment = std::nullopt, - const std::size_t indent_width = INDENT_WIDTH, - const std::size_t explicit_indent = 0, - const bool indent_before_chomping = false) -> void { + const std::size_t indicator = 0, const bool indent_before_chomping = false, + const YAMLRoundTrip *roundtrip = nullptr) -> void { stream.put(style == YAMLRoundTrip::ScalarStyle::Literal ? '|' : '>'); - if (indent_before_chomping && explicit_indent > 0) { - stream.put(static_cast('0' + explicit_indent)); + if (indent_before_chomping && indicator > 0) { + stream.put(static_cast('0' + indicator)); } if (chomping == YAMLRoundTrip::Chomping::Strip) { stream.put('-'); } else if (chomping == YAMLRoundTrip::Chomping::Keep) { stream.put('+'); } - if (!indent_before_chomping && explicit_indent > 0) { - stream.put(static_cast('0' + explicit_indent)); + if (!indent_before_chomping && indicator > 0) { + stream.put(static_cast('0' + indicator)); } if (header_comment.has_value()) { stream.put(' '); const auto &comment{header_comment.value()}; stream.write(comment.data(), static_cast(comment.size())); } - stream.put('\n'); + write_break(stream, roundtrip); std::size_t position{0}; while (position < value.size()) { auto line_end{value.find('\n', position)}; if (line_end == std::string::npos) { - write_indent(stream, indent, indent_width); + write_indent(stream, content_columns); stream.write(value.data() + position, static_cast(value.size() - position)); - stream.put('\n'); + write_break(stream, roundtrip); break; } if (line_end > position) { - write_indent(stream, indent, indent_width); + write_indent(stream, content_columns); } stream.write(value.data() + position, static_cast(line_end - position)); - stream.put('\n'); + write_break(stream, roundtrip); position = line_end + 1; } } @@ -277,6 +290,85 @@ inline auto matches_recorded_value(const YAMLRoundTrip::NodeStyle &style, same_value(style.content_value.value(), value); } +// The content indentation level of a block scalar that carries no indicator is +// read off its first non-empty line, so detection fails when that line begins +// with a space, and when the content holds no non-empty line at all. +// See https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator +inline auto block_detection_fails(const std::string &value) -> bool { + std::size_t position{0}; + while (position < value.size()) { + const auto line_end{value.find('\n', position)}; + const auto length{line_end == std::string::npos ? value.size() - position + : line_end - position}; + if (length > 0) { + return value[position] == ' '; + } + + if (line_end == std::string::npos) { + break; + } + + position = line_end + 1; + } + + return !value.empty(); +} + +// Every line a block scalar writes is closed with a line break, so the chomping +// indicator is what decides which of the trailing breaks of the value survive +// being read back. See +// https://yaml.org/spec/1.2.2/#8112-block-chomping-indicator +inline auto required_chomping(const std::string &value) + -> YAMLRoundTrip::Chomping { + const auto body{value.find_last_not_of('\n')}; + if (body == std::string::npos) { + return value.empty() ? YAMLRoundTrip::Chomping::Clip + : YAMLRoundTrip::Chomping::Keep; + } + + const auto breaks{value.size() - body - 1}; + if (breaks == 0) { + return YAMLRoundTrip::Chomping::Strip; + } + + return breaks == 1 ? YAMLRoundTrip::Chomping::Clip + : YAMLRoundTrip::Chomping::Keep; +} + +// Clipping and keeping both leave a single trailing line break in place, so a +// recorded indicator that only differs in that way still says the same thing +// and is worth writing back as it was +inline auto block_chomping(const YAMLRoundTrip::NodeStyle &style, + const std::string &value) + -> YAMLRoundTrip::Chomping { + const auto required{required_chomping(value)}; + if (!style.chomping.has_value() || style.chomping.value() == required) { + return required; + } + + return (required == YAMLRoundTrip::Chomping::Clip && !value.empty() && + style.chomping.value() == YAMLRoundTrip::Chomping::Keep) + ? YAMLRoundTrip::Chomping::Keep + : required; +} + +// The folded style joins the lines of its content, so it can only stand for a +// value that has no line break of its own to lose +inline auto folding_preserves(const std::string &value) -> bool { + const auto body{value.find_last_not_of('\n')}; + return body == std::string::npos || value.find('\n') == std::string::npos || + value.find('\n') > body; +} + +// The text a block scalar writes, which is the original one for as long as the +// document still holds the value it was read from +inline auto block_scalar_content(const YAMLRoundTrip::NodeStyle &style, + const JSON &value) -> const std::string & { + return style.block_content.has_value() && matches_recorded_value(style, value) + ? style.block_content.value() + : value.to_string(); +} + inline auto find_anchor(const AnchorValues &anchors, const std::string_view name) -> const JSON * { for (auto iterator{anchors.crbegin()}; iterator != anchors.crend(); @@ -320,6 +412,66 @@ inline auto write_anchor(OutputStream &stream, const std::string &name, anchors.emplace_back(name, &value); } +// A tag names the kind of value its node holds, so it may only be emitted +// while the document still holds that kind of value +inline auto matching_tag(const YAMLRoundTrip::NodeStyle *style, + const JSON &value) -> const std::string * { + if ((style == nullptr) || !style->tag.has_value() || + !style->tag_type.has_value() || style->tag_type.value() != value.type()) { + return nullptr; + } + + return &style->tag.value(); +} + +// Comments and node properties are read off a position in a sequence, so they +// only stand for what is written there while the sequence still holds the same +// items it was read from +inline auto keeps_item_annotations(const YAMLRoundTrip::NodeStyle *style, + const JSON &value) -> bool { + return (style == nullptr) || !style->sequence_size.has_value() || + style->sequence_size.value() == value.size(); +} + +inline auto has_node_properties(const YAMLRoundTrip::NodeStyle *style, + const JSON &value) -> bool { + return (matching_tag(style, value) != nullptr) || + ((style != nullptr) && style->anchor.has_value()); +} + +// Writes the tag and anchor that decorate a node, in the order they were +// written, and reports whether anything was written at all, as a node property +// has to be separated from the node it decorates +inline auto write_node_properties(OutputStream &stream, const JSON &value, + const YAMLRoundTrip::NodeStyle *style, + AnchorValues &anchors) -> bool { + const auto *tag{matching_tag(style, value)}; + const bool anchor{(style != nullptr) && style->anchor.has_value()}; + if ((tag == nullptr) && !anchor) { + return false; + } + + if ((tag != nullptr) && style->tag_before_anchor) { + stream.write(tag->data(), static_cast(tag->size())); + if (anchor) { + stream.put(' '); + } + } + + if (anchor) { + write_anchor(stream, style->anchor.value(), value, anchors); + } + + if ((tag != nullptr) && !style->tag_before_anchor) { + if (anchor) { + stream.put(' '); + } + stream.write(tag->data(), static_cast(tag->size())); + } + + return true; +} + inline auto write_string_with_style(OutputStream &stream, const JSON &value, const YAMLRoundTrip *roundtrip, const Pointer &pointer) -> void { @@ -508,32 +660,53 @@ inline auto is_implicit_null(const JSON &value, const YAMLRoundTrip *roundtrip, return !match->second.scalar.has_value(); } -inline auto write_flow_anchor(OutputStream &stream, const JSON &value, - const YAMLRoundTrip *roundtrip, - AnchorValues &anchors, const Pointer &pointer) +inline auto write_flow_properties(OutputStream &stream, const JSON &value, + const YAMLRoundTrip *roundtrip, + AnchorValues &anchors, const Pointer &pointer) -> void { if (roundtrip == nullptr) { return; } const auto match{roundtrip->styles.find(pointer)}; - if (match != roundtrip->styles.end() && match->second.anchor.has_value()) { - write_anchor(stream, match->second.anchor.value(), value, anchors); + if (match != roundtrip->styles.end() && + write_node_properties(stream, value, &match->second, anchors)) { stream.put(' '); } } +// The same as writing the properties of a flow node, but for a node that is +// written with no value at all, so nothing follows to be separated from +inline auto write_flow_node_properties(OutputStream &stream, const JSON &value, + const YAMLRoundTrip *roundtrip, + AnchorValues &anchors, + const Pointer &pointer) -> void { + if (roundtrip == nullptr) { + return; + } + + const auto match{roundtrip->styles.find(pointer)}; + if (match != roundtrip->styles.end()) { + write_node_properties(stream, value, &match->second, anchors); + } +} + inline auto write_flow_mapping(OutputStream &stream, const JSON &value, const YAMLRoundTrip *roundtrip, AnchorValues &anchors, Pointer &pointer) -> void { bool compact{false}; + bool padded{false}; if (roundtrip != nullptr) { const auto match{roundtrip->styles.find(pointer)}; if (match != roundtrip->styles.end()) { compact = match->second.compact_flow; + padded = match->second.padded_flow; } } stream.put('{'); + if (padded) { + stream.put(' '); + } bool first{true}; for (const auto &entry : value.as_object()) { if (!first) { @@ -547,12 +720,18 @@ inline auto write_flow_mapping(OutputStream &stream, const JSON &value, pointer.push_back(entry.first); write_key_string(stream, entry.first, roundtrip, pointer); stream.write(": ", 2); - if (!is_implicit_null(entry.second, roundtrip, pointer)) { - write_flow_anchor(stream, entry.second, roundtrip, anchors, pointer); + if (is_implicit_null(entry.second, roundtrip, pointer)) { + write_flow_node_properties(stream, entry.second, roundtrip, anchors, + pointer); + } else { + write_flow_properties(stream, entry.second, roundtrip, anchors, pointer); write_inline_value(stream, entry.second, roundtrip, anchors, pointer); } pointer.pop_back(); } + if (padded) { + stream.put(' '); + } stream.put('}'); } @@ -561,13 +740,20 @@ inline auto write_flow_sequence(OutputStream &stream, const JSON &value, AnchorValues &anchors, Pointer &pointer) -> void { bool compact{false}; + bool padded{false}; + bool annotations{true}; if (roundtrip != nullptr) { const auto match{roundtrip->styles.find(pointer)}; if (match != roundtrip->styles.end()) { compact = match->second.compact_flow; + padded = match->second.padded_flow; + annotations = keeps_item_annotations(&match->second, value); } } stream.put('['); + if (padded) { + stream.put(' '); + } bool first{true}; std::size_t item_index{0}; for (const auto &item : value.as_array()) { @@ -580,21 +766,26 @@ inline auto write_flow_sequence(OutputStream &stream, const JSON &value, } first = false; pointer.push_back(item_index); - write_flow_anchor(stream, item, roundtrip, anchors, pointer); + if (annotations) { + write_flow_properties(stream, item, roundtrip, anchors, pointer); + } write_inline_value(stream, item, roundtrip, anchors, pointer); pointer.pop_back(); item_index++; } + if (padded) { + stream.put(' '); + } stream.put(']'); } inline auto write_block_mapping(OutputStream &stream, const JSON &value, - std::size_t indent, bool skip_first_indent, + std::size_t columns, bool skip_first_indent, const YAMLRoundTrip *roundtrip, AnchorValues &anchors, Pointer &pointer) -> void; inline auto write_block_sequence(OutputStream &stream, const JSON &value, - std::size_t indent, bool skip_first_indent, + std::size_t columns, bool skip_first_indent, const YAMLRoundTrip *roundtrip, AnchorValues &anchors, Pointer &pointer) -> void; @@ -609,9 +800,18 @@ inline auto emit_inline_comment(OutputStream &stream, } inline auto write_node(OutputStream &stream, const JSON &value, - const std::size_t indent, const bool skip_first_indent, + const std::size_t columns, + const std::size_t block_indicator, + const bool skip_first_indent, const YAMLRoundTrip *roundtrip, AnchorValues &anchors, - Pointer &pointer) -> void { + Pointer &pointer, const bool skip_properties = false, + const bool annotations = true) -> void { + const auto width{(roundtrip != nullptr) ? roundtrip->indent_width + : INDENT_WIDTH}; + // Only the document root sits at the leftmost column, and a block scalar + // there has no column of its own, so its content is pushed one nesting level + // in to leave room for whatever follows it + const auto block_columns{columns == 0 ? width : columns}; const YAMLRoundTrip::NodeStyle *node_style{nullptr}; if (roundtrip != nullptr) { const auto style_match{roundtrip->styles.find(pointer)}; @@ -620,89 +820,112 @@ inline auto write_node(OutputStream &stream, const JSON &value, } } + const YAMLRoundTrip::NodeStyle *annotation_style{annotations ? node_style + : nullptr}; + const auto *alias{matching_alias(value, roundtrip, anchors, pointer)}; if (alias != nullptr) { write_alias(stream, *alias); - emit_inline_comment(stream, node_style); - stream.put('\n'); + emit_inline_comment(stream, annotation_style); + write_break(stream, roundtrip); return; } - bool has_anchor{false}; - if ((node_style != nullptr) && node_style->anchor.has_value()) { - write_anchor(stream, node_style->anchor.value(), value, anchors); - has_anchor = true; - } + const bool has_properties{ + skip_properties + ? false + : write_node_properties(stream, value, annotation_style, anchors)}; const bool flow{ (node_style != nullptr) && node_style->collection.has_value() && node_style->collection.value() == YAMLRoundTrip::CollectionStyle::Flow}; + // An indicator is a single digit, so content that needs one but sits too far + // in has to give up on block style and be quoted instead + const bool block_style{ + (node_style != nullptr) && value.is_string() && + node_style->scalar.has_value() && + (node_style->scalar.value() == YAMLRoundTrip::ScalarStyle::Literal || + node_style->scalar.value() == YAMLRoundTrip::ScalarStyle::Folded) && + ((block_indicator >= 1 && block_indicator <= 9) || + !block_detection_fails(block_scalar_content(*node_style, value)))}; + if (value.is_object() && !value.empty()) { if (flow) { - if (has_anchor) { + if (has_properties) { stream.put(' '); } write_flow_mapping(stream, value, roundtrip, anchors, pointer); - emit_inline_comment(stream, node_style); - stream.put('\n'); + emit_inline_comment(stream, annotation_style); + write_break(stream, roundtrip); } else { - if (has_anchor) { - emit_inline_comment(stream, node_style); - stream.put('\n'); + if (has_properties) { + emit_inline_comment(stream, annotation_style); + write_break(stream, roundtrip); } - write_block_mapping(stream, value, indent, - has_anchor ? false : skip_first_indent, roundtrip, + write_block_mapping(stream, value, columns, + has_properties ? false : skip_first_indent, roundtrip, anchors, pointer); } } else if (value.is_array() && !value.empty()) { if (flow) { - if (has_anchor) { + if (has_properties) { stream.put(' '); } write_flow_sequence(stream, value, roundtrip, anchors, pointer); - emit_inline_comment(stream, node_style); - stream.put('\n'); + emit_inline_comment(stream, annotation_style); + write_break(stream, roundtrip); } else { - if (has_anchor) { - emit_inline_comment(stream, node_style); - stream.put('\n'); + if (has_properties) { + emit_inline_comment(stream, annotation_style); + write_break(stream, roundtrip); } - write_block_sequence(stream, value, indent, - has_anchor ? false : skip_first_indent, roundtrip, - anchors, pointer); - } - } else if ((node_style != nullptr) && value.is_string() && - node_style->scalar.has_value() && - (node_style->scalar.value() == - YAMLRoundTrip::ScalarStyle::Literal || - node_style->scalar.value() == - YAMLRoundTrip::ScalarStyle::Folded)) { - if (has_anchor) { + // A block sequence may sit at the indentation of the mapping key it + // belongs to rather than one level further in + const auto sequence_columns{(node_style != nullptr) && + node_style->unindented_sequence && + columns >= block_indicator + ? columns - block_indicator + : columns}; + write_block_sequence(stream, value, sequence_columns, + has_properties ? false : skip_first_indent, + roundtrip, anchors, pointer); + } + } else if (block_style) { + if (has_properties) { stream.put(' '); } - const auto chomping{ - node_style->chomping.value_or(YAMLRoundTrip::Chomping::Clip)}; - const auto &content{node_style->block_content.has_value() && - matches_recorded_value(*node_style, value) - ? node_style->block_content.value() - : value.to_string()}; - write_block_scalar(stream, content, indent, node_style->scalar.value(), - chomping, node_style->comment_inline, - roundtrip->indent_width, node_style->explicit_indent, - node_style->indent_before_chomping); + const auto &content{block_scalar_content(*node_style, value)}; + const auto &text{value.to_string()}; + // The recorded style only reproduces the text it was read from, so once the + // document holds something else, a style that would lose a line break in + // the process gives way to one that keeps every line as it is + const bool original{node_style->block_content.has_value() && + matches_recorded_value(*node_style, value)}; + const auto style{original || folding_preserves(text) + ? node_style->scalar.value() + : YAMLRoundTrip::ScalarStyle::Literal}; + const std::optional header_comment{ + (annotation_style != nullptr) ? annotation_style->comment_inline + : std::nullopt}; + const bool indicated{block_detection_fails(content) || + node_style->explicit_indent > 0}; + write_block_scalar(stream, content, block_columns, style, + block_chomping(*node_style, text), header_comment, + indicated ? block_indicator : 0, + node_style->indent_before_chomping, roundtrip); } else { - if (has_anchor) { + if (has_properties) { stream.put(' '); } write_inline_value(stream, value, roundtrip, anchors, pointer); - emit_inline_comment(stream, node_style); - stream.put('\n'); + emit_inline_comment(stream, annotation_style); + write_break(stream, roundtrip); } } inline auto write_block_mapping(OutputStream &stream, const JSON &value, - const std::size_t indent, + const std::size_t columns, const bool skip_first_indent, const YAMLRoundTrip *roundtrip, AnchorValues &anchors, Pointer &pointer) @@ -729,16 +952,16 @@ inline auto write_block_mapping(OutputStream &stream, const JSON &value, if ((entry_style != nullptr) && !entry_style->comments_before.empty()) { for (const auto &comment : entry_style->comments_before) { if (comment.empty()) { - stream.put('\n'); + write_break(stream, roundtrip); } else { - write_indent(stream, indent, width); + write_indent(stream, columns); stream.write(comment.data(), static_cast(comment.size())); - stream.put('\n'); + write_break(stream, roundtrip); } } } - write_indent(stream, indent, width); + write_indent(stream, columns); } first = false; @@ -749,13 +972,12 @@ inline auto write_block_mapping(OutputStream &stream, const JSON &value, (roundtrip != nullptr) && entry.second.is_null() && !entry_is_alias && ((entry_style == nullptr) || !entry_style->scalar.has_value())}; if (implicit_null) { - if ((entry_style != nullptr) && entry_style->anchor.has_value()) { + if (has_node_properties(entry_style, entry.second)) { stream.put(' '); - write_anchor(stream, entry_style->anchor.value(), entry.second, - anchors); + write_node_properties(stream, entry.second, entry_style, anchors); } emit_inline_comment(stream, entry_style); - stream.put('\n'); + write_break(stream, roundtrip); } else { bool has_indicator_comment{false}; if ((entry_style != nullptr) && @@ -765,13 +987,12 @@ inline auto write_block_mapping(OutputStream &stream, const JSON &value, const auto &comment{entry_style->comment_on_indicator.value()}; stream.write(comment.data(), static_cast(comment.size())); - stream.put('\n'); - write_indent(stream, indent + 1, width); + write_break(stream, roundtrip); + write_indent(stream, columns + width); } if (!has_indicator_comment) { - const bool has_prefix{ - entry_is_alias || - ((entry_style != nullptr) && entry_style->anchor.has_value())}; + const bool has_prefix{entry_is_alias || + has_node_properties(entry_style, entry.second)}; const bool entry_flow{(entry_style != nullptr) && entry_style->collection.has_value() && entry_style->collection.value() == @@ -781,12 +1002,12 @@ inline auto write_block_mapping(OutputStream &stream, const JSON &value, !entry.second.empty() && !entry_flow && !has_prefix}; if (nested) { emit_inline_comment(stream, entry_style); - stream.put('\n'); + write_break(stream, roundtrip); } else { stream.put(' '); } } - write_node(stream, entry.second, indent + 1, + write_node(stream, entry.second, columns + width, width, has_indicator_comment ? true : false, roundtrip, anchors, pointer); } @@ -796,21 +1017,28 @@ inline auto write_block_mapping(OutputStream &stream, const JSON &value, } inline auto write_block_sequence(OutputStream &stream, const JSON &value, - const std::size_t indent, + const std::size_t columns, const bool skip_first_indent, const YAMLRoundTrip *roundtrip, AnchorValues &anchors, Pointer &pointer) -> void { assert(value.is_array() && !value.empty()); - const auto width{(roundtrip != nullptr) ? roundtrip->indent_width - : INDENT_WIDTH}; + const YAMLRoundTrip::NodeStyle *sequence_style{nullptr}; + if (roundtrip != nullptr) { + const auto style_match{roundtrip->styles.find(pointer)}; + if (style_match != roundtrip->styles.end()) { + sequence_style = &style_match->second; + } + } + + const bool annotations{keeps_item_annotations(sequence_style, value)}; bool first{true}; std::size_t item_index{0}; for (const auto &item : value.as_array()) { pointer.push_back(item_index); const YAMLRoundTrip::NodeStyle *item_style{nullptr}; - if (roundtrip != nullptr) { + if (annotations && (roundtrip != nullptr)) { const auto style_match{roundtrip->styles.find(pointer)}; if (style_match != roundtrip->styles.end()) { item_style = &style_match->second; @@ -824,16 +1052,16 @@ inline auto write_block_sequence(OutputStream &stream, const JSON &value, if ((item_style != nullptr) && !item_style->comments_before.empty()) { for (const auto &comment : item_style->comments_before) { if (comment.empty()) { - stream.put('\n'); + write_break(stream, roundtrip); } else { - write_indent(stream, indent, width); + write_indent(stream, columns); stream.write(comment.data(), static_cast(comment.size())); - stream.put('\n'); + write_break(stream, roundtrip); } } } - write_indent(stream, indent, width); + write_indent(stream, columns); } first = false; @@ -843,9 +1071,9 @@ inline auto write_block_sequence(OutputStream &stream, const JSON &value, if (implicit_null) { stream.put('-'); if (item_style != nullptr) { - if (item_style->anchor.has_value()) { + if (has_node_properties(item_style, item)) { stream.put(' '); - write_anchor(stream, item_style->anchor.value(), item, anchors); + write_node_properties(stream, item, item_style, anchors); } if (item_style->comment_on_indicator.has_value() && !item_style->comment_on_indicator.value().empty()) { @@ -856,7 +1084,7 @@ inline auto write_block_sequence(OutputStream &stream, const JSON &value, } } emit_inline_comment(stream, item_style); - stream.put('\n'); + write_break(stream, roundtrip); } else { bool has_indicator{false}; if ((item_style != nullptr) && @@ -870,13 +1098,15 @@ inline auto write_block_sequence(OutputStream &stream, const JSON &value, stream.write(comment.data(), static_cast(comment.size())); } - stream.put('\n'); - write_indent(stream, indent + 1, width); + write_break(stream, roundtrip); + write_indent(stream, columns + SEQUENCE_INDICATOR_WIDTH); } if (!has_indicator) { stream.write("- ", 2); } - write_node(stream, item, indent + 1, true, roundtrip, anchors, pointer); + write_node(stream, item, columns + SEQUENCE_INDICATOR_WIDTH, + SEQUENCE_INDICATOR_WIDTH, true, roundtrip, anchors, pointer, + false, annotations); } pointer.pop_back(); @@ -887,41 +1117,74 @@ inline auto write_block_sequence(OutputStream &stream, const JSON &value, template