// Glaze Library // For the license information refer to glaze.hpp #pragma once #include #include #include "glaze/core/common.hpp" #include "glaze/core/read.hpp" #include "glaze/core/reflect.hpp" #include "glaze/file/file_ops.hpp" #include "glaze/json/generic.hpp" #include "glaze/util/glaze_fast_float.hpp" #include "glaze/util/nullable_traits.hpp" #include "glaze/util/parse.hpp" #include "glaze/util/type_traits.hpp" #include "glaze/util/variant.hpp" #include "glaze/yaml/common.hpp" #include "glaze/yaml/opts.hpp" #include "glaze/yaml/skip.hpp" namespace glz { template <> struct parse { template static void op(T&& value, Ctx&& ctx, It0&& it, It1 end) { if constexpr (requires { ctx.stream_begin; }) { if (!ctx.stream_begin && it != end) { ctx.stream_begin = &*it; } } // Skip YAML directives and document start marker (---) if present yaml::skip_document_start(it, end, ctx); if (bool(ctx.error)) [[unlikely]] { return; } using V = std::remove_cvref_t; if (it == end) { // An empty document is a valid YAML null document. if constexpr (requires { value = nullptr; }) { value = nullptr; return; } else if constexpr (nullable_like) { value = {}; return; } else { ctx.error = error_code::unexpected_end; return; } } // A bare document boundary marker at root denotes an empty document. // Examples: "---\n---\n", "# comment\n...\n" if (yaml::at_document_start(it, end) || yaml::at_document_end(it, end)) { if constexpr (requires { value = nullptr; }) { value = nullptr; return; } else if constexpr (nullable_like) { value = {}; return; } else { ctx.error = error_code::unexpected_end; return; } } from::template op(std::forward(value), std::forward(ctx), std::forward(it), end); // A directive line (%YAML/%TAG/...) is only valid in the document prefix. // If parsing stopped before the end and we encounter a directive in the // remaining tail, treat it as malformed stream structure. if constexpr (!check_partial_read(Opts)) { if (!bool(ctx.error)) { auto is_document_start = [&](auto pos) { if (end - pos >= 3 && pos[0] == '-' && pos[1] == '-' && pos[2] == '-') { auto after = pos + 3; return after == end || *after == ' ' || *after == '\t' || *after == '\n' || *after == '\r' || *after == '#'; } return false; }; auto tail_scan = it; bool seen_document_end_marker = false; while (tail_scan != end) { auto line = tail_scan; while (line != end && (*line == ' ' || *line == '\t')) ++line; if (line != tail_scan && line != end && *line != '\n' && *line != '\r' && *line != '#') { // Indented tail usually belongs to continuation content. // But a plain "key: value" pattern here indicates malformed // trailing mapping content after a completed root node. const char first = *line; const bool explicit_or_structural_start = (first == ':' || first == '?' || first == '!' || first == '&' || first == '*' || first == '[' || first == '{' || first == '"' || first == '\'' || first == '-'); if (!explicit_or_structural_start) { auto scan = line; while (scan != end && *scan != '\n' && *scan != '\r') { if (*scan == ':') { auto after = scan + 1; if (after == end || *after == ' ' || *after == '\t' || *after == '\n' || *after == '\r') { ctx.error = error_code::syntax_error; return; } } ++scan; } } return; } if (line == end) { return; } if (*line == ':' || *line == '?') { return; // Explicit key/value continuation. } if (*line == '\n' || *line == '\r') { tail_scan = line; yaml::skip_newline(tail_scan, end); continue; } if (*line == '#') { while (tail_scan != end && *tail_scan != '\n' && *tail_scan != '\r') ++tail_scan; yaml::skip_newline(tail_scan, end); continue; } if (yaml::at_document_end(line, end)) { seen_document_end_marker = true; while (tail_scan != end && *tail_scan != '\n' && *tail_scan != '\r') ++tail_scan; yaml::skip_newline(tail_scan, end); continue; } if (is_document_start(line)) { // Additional documents are accepted, but document-start lines // that use non-standard tag handles (e.g. !prefix!Type) are // malformed in this single-document reader unless re-declared // for that document. Reject this shape in the stream tail. auto header = line + 3; while (header != end && (*header == ' ' || *header == '\t')) ++header; if (header != end && *header == '!') { auto tag_end = header + 1; while (tag_end != end && *tag_end != ' ' && *tag_end != '\t' && *tag_end != '\n' && *tag_end != '\r' && *tag_end != '#') { ++tag_end; } std::string_view tag_token(header, static_cast(tag_end - header)); const auto second_bang = tag_token.find('!', 1); if (second_bang != std::string_view::npos) { std::string_view handle = tag_token.substr(0, second_bang + 1); if (handle != "!" && handle != "!!") { ctx.error = error_code::syntax_error; return; } } } return; // Additional documents are allowed in the stream tail. } if (line != end && *line == '%') { // YAML directives are only valid in document prefixes. A directive // encountered mid-document (without a prior ...) is malformed. if (!seen_document_end_marker) { ctx.error = error_code::syntax_error; return; } return; } if (seen_document_end_marker) { return; // Implicit next document after explicit document end marker. } ctx.error = error_code::syntax_error; return; } } } } }; // glaze_value_t - unwrap custom value types template requires(glaze_value_t && !custom_read) struct from { template static void op(Value&& value, Ctx&& ctx, It0&& it, It1 end) { using V = std::decay_t(), meta_wrapper_v))>; from::template op(get_member(std::forward(value), meta_wrapper_v), std::forward(ctx), std::forward(it), end); } }; // Silently consume any value bound to a glz::skip sentinel. Reached when a // type opts out of serialization via meta::value = glz::skip{}. template <> struct from { template GLZ_ALWAYS_INLINE static void op(auto&&, is_context auto&& ctx, auto&&... args) noexcept { skip_value::template op(ctx, args...); } }; namespace yaml { // Handle YAML alias (*name) by replaying the stored anchor span. // Returns true if alias was handled (caller should return). // Returns false if current char is not '*' (caller continues normally). template bool handle_alias(T&& value, Ctx& ctx, It& it, [[maybe_unused]] End end) noexcept { if (it == end || *it != '*') return false; ++it; // skip '*' auto name = parse_anchor_name(it, end); if (name.empty()) { ctx.error = error_code::syntax_error; return true; } auto anchor_it = ctx.anchors.find(name); if (anchor_it == ctx.anchors.end()) { ctx.error = error_code::syntax_error; // undefined alias return true; } auto& span = anchor_it->second; // Empty anchor span (anchor on null/empty node) — leave value as default if (span.begin == span.end) { return true; } auto replay_it = span.begin; auto replay_end = span.end; // Save indent context and set up for replay auto saved_indent_stack = std::move(ctx.indent_stack); ctx.indent_stack.clear(); if (span.base_indent > 0) { ctx.push_indent(span.base_indent - 1); } using V = std::remove_cvref_t; from::template op(std::forward(value), ctx, replay_it, replay_end); // Restore indent context ctx.indent_stack = std::move(saved_indent_stack); return true; } template inline bool alias_token_is_mapping_key(It it, End end) noexcept { if (it == end || *it != '*') return false; ++it; // skip '*' auto name = parse_anchor_name(it, end); if (name.empty()) return false; skip_inline_ws(it, end); return (it != end && *it == ':') && ((it + 1) == end || whitespace_or_line_end_table[static_cast(*(it + 1))]); } struct node_property_state { bool has_anchor = false; std::string anchor_name{}; const char* anchor_start{}; int32_t anchor_indent = 0; }; struct node_property_policy { bool allow_alias = true; bool alias_can_be_mapping_key = false; bool allow_empty_after_anchor = false; bool disallow_anchor_on_alias = false; }; struct node_preamble_state { yaml_tag tag = yaml_tag::none; node_property_state node_props{}; }; // Parse alias/anchor node properties shared across YAML value parsers. // Returns true when caller should stop (alias consumed, tolerated empty anchor, or syntax/error). template inline bool parse_node_properties(T&& value, Ctx& ctx, It& it, End end, node_property_state& state) noexcept { state.has_anchor = false; state.anchor_name.clear(); state.anchor_start = nullptr; state.anchor_indent = ctx.current_indent(); if constexpr (Policy.allow_alias) { if constexpr (Policy.alias_can_be_mapping_key) { if (!alias_token_is_mapping_key(it, end)) { if (handle_alias(std::forward(value), ctx, it, end)) return true; } } else { if (handle_alias(std::forward(value), ctx, it, end)) return true; } } if (it != end && *it == '&') { ++it; auto name = parse_anchor_name(it, end); if (name.empty()) { ctx.error = error_code::syntax_error; return true; } skip_inline_ws(it, end); if (it == end) { if constexpr (!Policy.allow_empty_after_anchor) { ctx.error = error_code::unexpected_end; } return true; } if constexpr (Policy.disallow_anchor_on_alias) { if (*it == '*') { ctx.error = error_code::syntax_error; return true; } } state.has_anchor = true; state.anchor_name = std::string(name); state.anchor_start = &*it; } return false; } template inline void finalize_node_anchor(node_property_state& state, Ctx& ctx, It it) noexcept { if (state.has_anchor && !bool(ctx.error)) { ctx.anchors[std::move(state.anchor_name)] = {state.anchor_start, &*it, state.anchor_indent}; } } template inline bool parse_node_preamble(T&& value, Ctx& ctx, It& it, End end, node_preamble_state& preamble, TagValidator&& tag_validator) noexcept { skip_inline_ws(it, end); if (it == end) [[unlikely]] { if constexpr (!AllowEmptyInput) { ctx.error = error_code::unexpected_end; } return true; } preamble.tag = parse_yaml_tag(it, end, ctx); if (preamble.tag == yaml_tag::unknown) [[unlikely]] { ctx.error = error_code::syntax_error; return true; } if (!tag_validator(preamble.tag)) [[unlikely]] { ctx.error = error_code::syntax_error; return true; } skip_inline_ws(it, end); if (it == end) [[unlikely]] { if constexpr (!AllowEmptyInput) { ctx.error = error_code::unexpected_end; } return true; } if (parse_node_properties(std::forward(value), ctx, it, end, preamble.node_props)) { return true; } return false; } // SWAR-optimized double-quoted string parsing // Uses 8-byte chunks for fast scanning and copying template inline void parse_double_quoted_string(std::string& value, Ctx& ctx, It& it, End end) { static constexpr size_t string_padding_bytes = 8; if (it == end || *it != '"') [[unlikely]] { ctx.error = error_code::expected_quote; return; } ++it; // skip opening quote auto start = it; // Pass 1: Find closing quote byte-by-byte (need to handle newlines and escapes) while (it != end && *it != '"') { if (*it == '\\') { ++it; if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } } ++it; } if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } // Allocate buffer with room for potential expansion and SWAR padding // Some YAML escapes expand: \L, \P -> 3 bytes UTF-8 const auto input_len = static_cast(it - start); value.resize(input_len + (input_len / 2) + string_padding_bytes); auto* dst = value.data(); auto* const dst_start = dst; // Pass 2: Copy and process escapes and line folding auto src = start; const auto* const src_end = &*it; // Track position past which we should not trim. Escape-produced characters // (even whitespace like \t) are content and must not be trimmed by line folding. auto* last_non_trimmable = dst; while (src < src_end) { // Check for newline - needs line folding if (*src == '\n' || *src == '\r') { // Trim trailing LITERAL whitespace from output before processing newline. // Escape-produced whitespace (e.g. from \t) is preserved. dst = (std::max)(last_non_trimmable, dst_start); // Skip the newline if (*src == '\r' && (src + 1) < src_end && *(src + 1) == '\n') { src += 2; // CRLF } else { ++src; } // Skip leading indentation on the next line. int indent_count = 0; if (!yaml::skip_folded_line_indent(src, src_end, ctx, &indent_count)) return; if (indent_count == 0 && yaml::starts_with_document_marker(src, src_end)) { ctx.error = error_code::syntax_error; return; } // Check if this is a blank line (another newline follows) if (src < src_end && (*src == '\n' || *src == '\r')) { // Blank line(s) - output newlines for each blank line while (src < src_end && (*src == '\n' || *src == '\r')) { *dst++ = '\n'; // Skip the newline if (*src == '\r' && (src + 1) < src_end && *(src + 1) == '\n') { src += 2; // CRLF } else { ++src; } int blank_indent_count = 0; if (!yaml::skip_folded_line_indent(src, src_end, ctx, &blank_indent_count)) return; if (blank_indent_count == 0 && yaml::starts_with_document_marker(src, src_end)) { ctx.error = error_code::syntax_error; return; } } // Don't add space - we're now at content after blank line(s) } else { // Single newline - fold to space *dst++ = ' '; } last_non_trimmable = dst; continue; } if (*src == '\\') { ++src; if (src >= src_end) [[unlikely]] { // Shouldn't happen - we validated in pass 1 ctx.error = error_code::syntax_error; return; } const unsigned char esc = static_cast(*src); // Check for escaped newline (line continuation - no space) if (esc == '\n' || esc == '\r') { // Skip the newline if (esc == '\r' && (src + 1) < src_end && *(src + 1) == '\n') { src += 2; // CRLF } else { ++src; } int indent_count = 0; if (!yaml::skip_folded_line_indent(src, src_end, ctx, &indent_count)) return; if (indent_count == 0 && yaml::starts_with_document_marker(src, src_end)) { ctx.error = error_code::syntax_error; return; } // No output - this is line continuation without space last_non_trimmable = dst; continue; } // Check simple escape table first if (yaml_escape_is_simple[esc]) { *dst++ = yaml_unescape_table[esc]; ++src; } // Handle escapes requiring special processing else if (yaml_escape_needs_special[esc]) { ++src; // skip escape char switch (esc) { case 'x': { // \xXX - 2 hex digits if (static_cast(src_end - src) < 2) [[unlikely]] { ctx.error = error_code::syntax_error; return; } const uint32_t hi = digit_hex_table[static_cast(src[0])]; const uint32_t lo = digit_hex_table[static_cast(src[1])]; if ((hi | lo) & 0xF0) [[unlikely]] { ctx.error = error_code::syntax_error; return; } const uint32_t codepoint = (hi << 4) | lo; src += 2; dst += code_point_to_utf8(codepoint, dst); break; } case 'u': { // \uXXXX - 4 hex digits if (static_cast(src_end - src) < 4) [[unlikely]] { ctx.error = error_code::syntax_error; return; } const uint32_t codepoint = hex_to_u32(src); if (codepoint == 0xFFFFFFFFu) [[unlikely]] { ctx.error = error_code::syntax_error; return; } src += 4; dst += code_point_to_utf8(codepoint, dst); break; } case 'U': { // \UXXXXXXXX - 8 hex digits if (static_cast(src_end - src) < 8) [[unlikely]] { ctx.error = error_code::syntax_error; return; } const uint32_t hi = hex_to_u32(src); const uint32_t lo = hex_to_u32(src + 4); if ((hi | lo) == 0xFFFFFFFFu) [[unlikely]] { ctx.error = error_code::syntax_error; return; } const uint32_t codepoint = (hi << 16) | lo; src += 8; if (codepoint <= 0x10FFFF) { dst += code_point_to_utf8(codepoint, dst); } break; } case 'N': { // Next line U+0085 dst += code_point_to_utf8(0x0085, dst); break; } case '_': { // Non-breaking space U+00A0 dst += code_point_to_utf8(0x00A0, dst); break; } case 'L': { // Line separator U+2028 dst += code_point_to_utf8(0x2028, dst); break; } case 'P': { // Paragraph separator U+2029 dst += code_point_to_utf8(0x2029, dst); break; } default: break; } } else { // Preserve unknown escapes for compatibility, but reject a subset that // YAML test-suite marks as malformed in double-quoted scalars. if (esc == '.' || esc == '\'') { ctx.error = error_code::syntax_error; return; } *dst++ = '\\'; *dst++ = static_cast(esc); ++src; } // All escape-produced characters are content, not trimmable last_non_trimmable = dst; } else { const char ch = *src++; *dst++ = ch; // Only update non-trimmable for non-whitespace literal characters if (ch != ' ' && ch != '\t') { last_non_trimmable = dst; } } } // Resize to actual length value.resize(static_cast(dst - dst_start)); ++it; // skip closing quote } // Single-quoted string parsing with line folding // Only escape is '' -> ' (doubled single quote) // Line breaks are folded: single newline -> space, blank line -> newline template inline void parse_single_quoted_string(std::string& value, Ctx& ctx, It& it, End end) { static constexpr size_t string_padding_bytes = 8; if (it == end || *it != '\'') [[unlikely]] { ctx.error = error_code::expected_quote; return; } ++it; // skip opening quote auto start = it; // Pass 1: Find closing quote (handling '' escapes) while (it != end) { if (*it == '\'') { if ((it + 1) != end && *(it + 1) == '\'') { it += 2; // Skip escaped quote } else { break; // Found closing quote } } else { ++it; } } if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } const auto input_len = static_cast(it - start); value.resize(input_len + string_padding_bytes); auto* dst = value.data(); auto* const dst_start = dst; auto src = start; const auto* const src_end = &*it; // Pass 2: Process content with line folding and '' escapes while (src < src_end) { // Check for newline - needs line folding if (*src == '\n' || *src == '\r') { // Trim trailing whitespace from output before processing newline while (dst > dst_start && (*(dst - 1) == ' ' || *(dst - 1) == '\t')) { --dst; } // Skip the newline if (*src == '\r' && (src + 1) < src_end && *(src + 1) == '\n') { src += 2; // CRLF } else { ++src; } // Skip leading indentation on the next line. int indent_count = 0; if (!yaml::skip_folded_line_indent(src, src_end, ctx, &indent_count)) return; if (indent_count == 0 && yaml::starts_with_document_marker(src, src_end)) { ctx.error = error_code::syntax_error; return; } // Check if this is a blank line (another newline follows) if (src < src_end && (*src == '\n' || *src == '\r')) { // Blank line(s) - output newlines for each blank line while (src < src_end && (*src == '\n' || *src == '\r')) { *dst++ = '\n'; // Skip the newline if (*src == '\r' && (src + 1) < src_end && *(src + 1) == '\n') { src += 2; // CRLF } else { ++src; } int blank_indent_count = 0; if (!yaml::skip_folded_line_indent(src, src_end, ctx, &blank_indent_count)) return; if (blank_indent_count == 0 && yaml::starts_with_document_marker(src, src_end)) { ctx.error = error_code::syntax_error; return; } } // Don't add space - we're now at content after blank line(s) } else { // Single newline - fold to space *dst++ = ' '; } continue; } if (*src == '\'') { // Must be '' (escaped quote) - we validated this in pass 1 *dst++ = '\''; src += 2; } else { *dst++ = *src++; } } value.resize(static_cast(dst - dst_start)); ++it; // skip closing quote } template inline void skip_flow_ws_and_newlines(Ctx& ctx, It& it, End end, bool* saw_line_break = nullptr) { bool at_line_start = false; bool saw_separation_ws = false; bool line_has_indent = false; while (it != end) { if (*it == ' ') { ++it; saw_separation_ws = true; if (at_line_start) line_has_indent = true; continue; } if (*it == '\t') { if (at_line_start && !line_has_indent) { auto probe = it; while (probe != end && (*probe == ' ' || *probe == '\t')) ++probe; if (probe != end && *probe != '\n' && *probe != '\r' && *probe != '#') { const char c = *probe; const bool allowed_flow_delim = (c == ']' || c == '}' || c == '[' || c == '{' || c == ','); if (!allowed_flow_delim) { ctx.error = error_code::syntax_error; return; } } } ++it; saw_separation_ws = true; if (at_line_start) line_has_indent = true; continue; } if (*it == '\n' || *it == '\r') { skip_newline(it, end); if (saw_line_break) *saw_line_break = true; at_line_start = true; saw_separation_ws = true; line_has_indent = false; continue; } if (*it == '#') { // In flow style, comments require separation from the previous token. // Allow comment-only lines and comments after inline whitespace, but // reject adjacency forms like ",#comment" (CVW2). bool separated_by_prev_ws = false; if constexpr (std::is_pointer_v>) { if (ctx.stream_begin && it > ctx.stream_begin) { const char prev = *(it - 1); separated_by_prev_ws = (prev == ' ' || prev == '\t'); } } if (!(at_line_start || saw_separation_ws || separated_by_prev_ws)) { break; } skip_comment(it, end); if (it != end && (*it == '\n' || *it == '\r')) { skip_newline(it, end); if (saw_line_break) *saw_line_break = true; at_line_start = true; saw_separation_ws = true; continue; } continue; } // In block context, multiline flow nodes require indentation on content // continuation lines (except structural tokens). if (at_line_start && ctx.current_indent() >= 0 && !line_has_indent) { const char c = *it; const bool structural = (c == ']' || c == '}' || c == ','); if (!structural) { ctx.error = error_code::syntax_error; return; } } break; } } template inline void validate_flow_node_adjacent_tail(Ctx& ctx, It& it, End end) { if (it == end) return; const char c = *it; // After a flow collection closes, the next character must be a structural // separator or whitespace/comment. Adjacent plain content is malformed. if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '#' || c == ',' || c == ']' || c == '}' || c == ':') { return; } ctx.error = error_code::syntax_error; } // At root level, a closed flow collection must be followed only by: // inline spaces, optional inline comment, newline-separated comments/blank lines, // or stream separators (--- / ...). This catches malformed trailing content such as: // "[a] ]", "[a]#comment" (without separation), or "[a]\ntrailing". template inline void validate_root_flow_tail_after_close(Ctx& ctx, It& it, End end) { auto is_document_start = [&](auto pos) { if (end - pos >= 3 && pos[0] == '-' && pos[1] == '-' && pos[2] == '-') { auto after = pos + 3; return after == end || *after == ' ' || *after == '\t' || *after == '\n' || *after == '\r' || *after == '#'; } return false; }; bool at_line_start = false; auto tail = it; while (tail != end) { auto line = tail; skip_inline_ws(line, end); if (line == end) { return; } if (*line == '\n' || *line == '\r') { tail = line; skip_newline(tail, end); at_line_start = true; continue; } if (*line == '#') { if (!at_line_start && line == it) { ctx.error = error_code::syntax_error; return; } tail = line; skip_comment(tail, end); if (tail != end && (*tail == '\n' || *tail == '\r')) { skip_newline(tail, end); at_line_start = true; continue; } return; } if (at_line_start && (at_document_end(line, end) || is_document_start(line))) { return; } ctx.error = error_code::syntax_error; return; } } // Parse a plain scalar (unquoted) template inline void parse_plain_scalar(std::string& value, Ctx& ctx, It& it, End end, bool in_flow) { value.clear(); while (it != end) { const char c = *it; // End conditions if (c == '\n' || c == '\r') { if (in_flow) { auto continuation = it; skip_newline(continuation, end); bool continuation_is_comment = false; while (continuation != end) { while (continuation != end && (*continuation == ' ' || *continuation == '\t')) { ++continuation; } if (continuation != end && *continuation == '#') { // Comment lines terminate plain flow scalars; the caller // will consume comments/separators outside this scalar. continuation_is_comment = true; break; } break; } if (continuation_is_comment || continuation == end || *continuation == ',' || *continuation == ']' || *continuation == '}' || *continuation == ':') { break; } while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) { value.pop_back(); } if (!value.empty()) { value.push_back(' '); } it = continuation; continue; } break; } // Per YAML spec: # only starts a comment when preceded by whitespace // "foo#bar" is a valid plain scalar, but "foo #bar" has a comment if (c == '#') { // Check if preceded by whitespace (comment indicator) if (value.empty() || value.back() == ' ' || value.back() == '\t') { break; // This is a comment } // Otherwise # is part of the scalar value } // Flow indicators end plain scalars in flow context if (in_flow && (c == ',' || c == ']' || c == '}')) break; // Colon followed by space/newline ends plain scalar if (c == ':') { if ((it + 1) == end || *(it + 1) == ' ' || *(it + 1) == '\t' || *(it + 1) == '\n' || *(it + 1) == '\r') { break; } if (in_flow && (*(it + 1) == ',' || *(it + 1) == ']' || *(it + 1) == '}')) { break; } } value.push_back(c); ++it; } // Trim trailing whitespace while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) { value.pop_back(); } if (in_flow && (value == "---" || value == "..." || value == "-")) { ctx.error = error_code::syntax_error; } } // Parse a multiline plain scalar with folding (for block context) // This handles plain scalars that span multiple lines, where continuation // lines are indented more than the base indent level. // Per YAML spec: // - Continuation lines must be more indented than the base indent // - A single newline between lines becomes a single space // - Blank lines are preserved as literal newlines template inline void parse_plain_scalar_multiline(std::string& value, Ctx& ctx, It& it, End end, int32_t base_indent) { value.clear(); while (it != end) { const char c = *it; // Check for newline - potential continuation if (c == '\n' || c == '\r') { // Trim trailing whitespace from current line while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) { value.pop_back(); } // Count consecutive blank lines int blank_lines = 0; auto lookahead = it; while (lookahead != end) { // Skip the newline if (*lookahead == '\r' && (lookahead + 1) != end && *(lookahead + 1) == '\n') { ++lookahead; } if (lookahead != end && (*lookahead == '\n' || *lookahead == '\r')) { ++lookahead; } else { break; } // Check if this is a blank line or comment-only line int32_t line_indent = 0; while (lookahead != end && (*lookahead == ' ' || *lookahead == '\t')) { ++line_indent; ++lookahead; } if (lookahead == end || *lookahead == '\n' || *lookahead == '\r') { // Blank line ++blank_lines; continue; } if (*lookahead == '#') { // Comments end plain scalars per YAML spec return; } // Found content - check indentation // Continuation lines must not dedent below the parent block. if (line_indent < base_indent) { // Dedented - end of scalar return; } // Document boundary markers at column 0 start/stop documents and // must terminate a top-level plain scalar continuation. if (line_indent == 0 && (at_document_start(lookahead, end) || at_document_end(lookahead, end))) { return; } // Sequence indicator (- followed by space/newline/end). // Terminates the scalar when the dash is at or above the // enclosing sequence's indicator column, meaning it starts // a sibling entry. A dash deeper than the indicator is // plain content (yaml-test-suite AB8U). if (*lookahead == '-') { auto after_dash = lookahead + 1; if (after_dash == end || *after_dash == ' ' || *after_dash == '\t' || *after_dash == '\n' || *after_dash == '\r') { if (ctx.sequence_dash_indent < 0 || line_indent <= ctx.sequence_dash_indent) { return; } } } if (ctx.explicit_mapping_key_context) { // In explicit mapping keys ("? key"), a following explicit // key/value indicator starts a new mapping entry. if (*lookahead == '?' || *lookahead == ':') { auto after = lookahead + 1; if (after == end || *after == ' ' || *after == '\t' || *after == '\n' || *after == '\r') { return; } } // Node-property indicators at the start of a continuation // line begin a new key node in explicit-key context. if (*lookahead == '&' || *lookahead == '*' || *lookahead == '!') { return; } } // In a "- item" value context, an anchor/tag/alias at the start // of a continuation candidate starts a new node, not scalar content. if (ctx.sequence_item_value_context && (*lookahead == '&' || *lookahead == '*' || *lookahead == '!')) { return; } // Check for mapping key indicator (content followed by : and space) // This is a heuristic - we look for ": " pattern which indicates a mapping key { auto scan = lookahead; while (scan != end && *scan != '\n' && *scan != '\r') { if (*scan == ':') { auto after_colon = scan + 1; if (after_colon == end || *after_colon == ' ' || *after_colon == '\t' || *after_colon == '\n' || *after_colon == '\r') { // This looks like a mapping key - end the scalar return; } } ++scan; } } // This is a continuation line // Add appropriate line breaks for blank lines for (int i = 0; i < blank_lines; ++i) { value.push_back('\n'); } // Add a space to fold the newline (unless we added literal newlines for blank lines) if (blank_lines == 0 && !value.empty()) { value.push_back(' '); } // Move iterator to the content position it = lookahead; break; } // If we exhausted the lookahead, we're done if (lookahead == end) { return; } continue; // Process the character at the new position } // Per YAML spec: # only starts a comment when preceded by whitespace if (c == '#') { if (value.empty() || value.back() == ' ' || value.back() == '\t') { break; // This is a comment } } // Colon followed by space/newline ends plain scalar (potential nested mapping) if (c == ':') { if ((it + 1) == end || *(it + 1) == ' ' || *(it + 1) == '\t' || *(it + 1) == '\n' || *(it + 1) == '\r') { break; } } value.push_back(c); ++it; } // Trim trailing whitespace while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) { value.pop_back(); } } // Parse a block scalar (| or >) template inline void parse_block_scalar(std::string& value, Ctx& ctx, It& it, End end, int32_t base_indent) { if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } const char indicator = *it; ++it; const auto header_start = it; // Chomping indicator: - (strip), + (keep), or none (clip) char chomping = ' '; // default: clip int explicit_indent = 0; bool seen_chomping = false; bool seen_indent = false; while (it != end && *it != '\n' && *it != '\r') { if (*it == '-' || *it == '+') { if (seen_chomping) { ctx.error = error_code::syntax_error; return; } seen_chomping = true; chomping = *it; } else if (*it >= '1' && *it <= '9') { if (seen_indent) { // YAML indentation indicator is a single digit [1-9]. ctx.error = error_code::syntax_error; return; } seen_indent = true; explicit_indent = *it - '0'; } else if (*it == '0') { // Indentation indicator 0 is invalid by spec. ctx.error = error_code::syntax_error; return; } else if (*it == ' ' || *it == '\t') { // Skip whitespace } else if (*it == '#') { // Block scalar comments require separation whitespace. if (it == header_start || (*(it - 1) != ' ' && *(it - 1) != '\t')) { ctx.error = error_code::syntax_error; return; } // Skip comment while (it != end && *it != '\n' && *it != '\r') { ++it; } break; } else { ctx.error = error_code::syntax_error; return; } ++it; } // Skip newline after indicator if (!skip_newline(it, end)) { // Empty block scalar value.clear(); return; } value.clear(); // Determine content indentation int32_t content_indent = -1; int32_t leading_blank_indent_max = -1; bool first_line = true; bool previous_line_starts_with_tab = false; bool previous_line_more_indented = false; std::string trailing_newlines; while (it != end) { auto line_start = it; int32_t line_indent = measure_indent(it, end, ctx); if (bool(ctx.error)) [[unlikely]] return; // Check for blank line if (it == end || *it == '\n' || *it == '\r') { // In literal mode, a line with more spaces than content_indent // has whitespace content that must be preserved. if (content_indent >= 0 && line_indent > content_indent && indicator == '|') { it = line_start; for (int32_t i = 0; i < content_indent && it != end && *it == ' '; ++i) { ++it; } // Fall through to content processing below } else { if (content_indent < 0) { leading_blank_indent_max = (std::max)(leading_blank_indent_max, line_indent); } trailing_newlines.push_back('\n'); skip_newline(it, end); continue; } } // Top-level zero-indented block scalars must stop at document boundary markers. if (base_indent < 0 && line_indent == 0 && (at_document_start(it, end) || at_document_end(it, end))) { it = line_start; break; } // First content line determines indentation if (content_indent < 0) { if (explicit_indent > 0) { content_indent = base_indent + explicit_indent; } else { content_indent = line_indent; } if (content_indent <= base_indent) { it = line_start; break; } if (leading_blank_indent_max > content_indent) { ctx.error = error_code::syntax_error; return; } } // Check if we've dedented if (line_indent < content_indent) { it = line_start; break; } // Skip to content_indent level it = line_start; for (int32_t i = 0; i < content_indent && it != end && *it == ' '; ++i) { ++it; } const bool current_line_starts_with_tab = (it != end && *it == '\t'); const bool current_line_more_indented = (line_indent > content_indent); // Add previous newlines if (first_line) { // Leading blank lines are part of block scalar content value += trailing_newlines; } else { if (indicator == '|') { // Literal: preserve newlines value += trailing_newlines; } else { // Folded: single newline becomes space, paragraph breaks keep one newline. // Folding does not apply adjacent to "more indented" or tab-leading lines. const size_t break_count = trailing_newlines.size(); const bool adjacent_special = previous_line_starts_with_tab || current_line_starts_with_tab || previous_line_more_indented || current_line_more_indented; if (break_count == 1) { if (adjacent_special) { value.push_back('\n'); } else { value.push_back(' '); } } else if (break_count > 1) { const size_t preserve_count = adjacent_special ? break_count : (break_count - 1); value.append(preserve_count, '\n'); } } } trailing_newlines.clear(); first_line = false; previous_line_starts_with_tab = current_line_starts_with_tab; previous_line_more_indented = current_line_more_indented; // Read line content while (it != end && *it != '\n' && *it != '\r') { value.push_back(*it); ++it; } trailing_newlines.push_back('\n'); skip_newline(it, end); } // Apply chomping if (chomping == '-') { // Strip: remove all trailing newlines } else if (chomping == '+') { // Keep: preserve all trailing newlines value += trailing_newlines; } else { // Clip: single trailing newline if (!value.empty()) { value.push_back('\n'); } } } // Parse a YAML key (unquoted or quoted) template inline bool parse_yaml_key(std::string& key, Ctx& ctx, It& it, End end, bool in_flow) { key.clear(); skip_inline_ws(it, end); if (it == end) { ctx.error = error_code::unexpected_end; return false; } auto validate_key_tag = [&](const yaml_tag tag) { if (tag == yaml_tag::unknown) { ctx.error = error_code::syntax_error; return false; } if (!tag_valid_for_string(tag)) { ctx.error = error_code::syntax_error; return false; } return true; }; // Tags on keys (e.g. "!!str : value") auto key_tag = parse_yaml_tag(it, end, ctx); if (!validate_key_tag(key_tag)) { return false; } skip_inline_ws(it, end); if (it == end) { ctx.error = error_code::unexpected_end; return false; } // Handle alias as key (*name resolves to anchor value) if (*it == '*') { ++it; auto name = parse_anchor_name(it, end); if (name.empty()) { ctx.error = error_code::syntax_error; return false; } auto anchor_it = ctx.anchors.find(name); if (anchor_it == ctx.anchors.end()) { ctx.error = error_code::syntax_error; // undefined alias return false; } auto& span = anchor_it->second; if (span.begin == span.end) { key.clear(); // empty anchor } else { // Replay the anchor span to extract the key string auto replay_it = span.begin; auto replay_end = span.end; if (*replay_it == '"') { parse_double_quoted_string(key, ctx, replay_it, replay_end); } else if (*replay_it == '\'') { parse_single_quoted_string(key, ctx, replay_it, replay_end); } else if (*replay_it == '[' || *replay_it == '{') { // Canonicalize complex alias keys (flow seq/map) to a stable string form. glz::generic key_node{}; auto temp_ctx = ctx.make_speculative(); from::template op()>( key_node, temp_ctx, replay_it, replay_end); if (bool(temp_ctx.error)) [[unlikely]] { ctx.error = temp_ctx.error; return false; } ctx.anchors = std::move(temp_ctx.anchors); if (auto* s = key_node.template get_if()) { key = *s; } else { (void)glz::write_json(key_node, key); } } else { bool parse_structured_alias_key = false; if (*replay_it == '-' && ((replay_it + 1) == replay_end || yaml::whitespace_or_line_end_table[static_cast(*(replay_it + 1))])) { parse_structured_alias_key = true; } else { auto scan = replay_it; while (scan != replay_end) { if (*scan == ':') { const auto after_colon = scan + 1; if (after_colon == replay_end || yaml::whitespace_or_line_end_table[static_cast(*after_colon)]) { parse_structured_alias_key = true; break; } } if (*scan == '\n' || *scan == '\r') { break; } ++scan; } } if (!parse_structured_alias_key) { for (auto p = replay_it; p != replay_end; ++p) { if (*p == '\n' || *p == '\r') { parse_structured_alias_key = true; break; } } } if (parse_structured_alias_key) { glz::generic key_node{}; auto temp_ctx = ctx.make_speculative(); from::template op(key_node, temp_ctx, replay_it, replay_end); if (bool(temp_ctx.error)) [[unlikely]] { ctx.error = temp_ctx.error; return false; } ctx.anchors = std::move(temp_ctx.anchors); if (auto* s = key_node.template get_if()) { key = *s; } else { (void)glz::write_json(key_node, key); } } else { parse_plain_scalar(key, ctx, replay_it, replay_end, false); } } } return !bool(ctx.error); } // Handle anchor on key (&name before key text) bool has_key_anchor = false; std::string key_anchor_name; if (*it == '&') { ++it; auto name = parse_anchor_name(it, end); if (name.empty()) { ctx.error = error_code::syntax_error; return false; } skip_inline_ws(it, end); if (it == end) { ctx.error = error_code::unexpected_end; return false; } // Anchor on alias is invalid per YAML spec if (*it == '*') { ctx.error = error_code::syntax_error; return false; } has_key_anchor = true; key_anchor_name = std::string(name); } // Allow node-property order "&anchor !!str key: value" for mapping keys. if (*it == '!') { if (key_tag != yaml_tag::none) { // Duplicate tag properties on the same key node are malformed. ctx.error = error_code::syntax_error; return false; } key_tag = parse_yaml_tag(it, end, ctx); if (!validate_key_tag(key_tag)) { return false; } skip_inline_ws(it, end); if (it == end) { ctx.error = error_code::unexpected_end; return false; } } const char* key_start = &*it; bool quoted_key_spans_lines = false; if (*it == '"') { auto quoted_start = it; parse_double_quoted_string(key, ctx, it, end); if (!bool(ctx.error)) { for (auto p = quoted_start; p != it; ++p) { if (*p == '\n' || *p == '\r') { quoted_key_spans_lines = true; break; } } } } else if (*it == '\'') { auto quoted_start = it; parse_single_quoted_string(key, ctx, it, end); if (!bool(ctx.error)) { for (auto p = quoted_start; p != it; ++p) { if (*p == '\n' || *p == '\r') { quoted_key_spans_lines = true; break; } } } } else { // Plain key - read until colon while (it != end) { const char c = *it; if (c == ':') { // Check if this ends the key if ((it + 1) == end || *(it + 1) == ' ' || *(it + 1) == '\t' || *(it + 1) == '\n' || *(it + 1) == '\r') { break; } if (in_flow && (*(it + 1) == ',' || *(it + 1) == ']' || *(it + 1) == '}')) { break; } } if (c == '\n' || c == '\r') { if (!in_flow) break; auto continuation = it; skip_newline(continuation, end); int32_t continuation_indent = 0; while (continuation != end && (*continuation == ' ' || *continuation == '\t')) { if (*continuation == ' ') ++continuation_indent; ++continuation; } if (continuation == end) { break; } if (*continuation == '#') { it = continuation; skip_comment(it, end); if (it != end && (*it == '\n' || *it == '\r')) { skip_newline(it, end); continue; } break; } // A continuation line starting with flow terminators or ':' does // not belong to the key content. if (*continuation == ',' || *continuation == ']' || *continuation == '}' || *continuation == ':') { break; } if (ctx.explicit_mapping_key_context) { if (*continuation == '?' || *continuation == ':') { auto after = continuation + 1; if (after == end || *after == ' ' || *after == '\t' || *after == '\n' || *after == '\r') { break; } } if (*continuation == '&' || *continuation == '*' || *continuation == '!') { break; } if (*continuation == '-') { auto after = continuation + 1; if (after == end || *after == ' ' || *after == '\t' || *after == '\n' || *after == '\r') { break; } } if (ctx.current_indent() >= 0 && continuation_indent <= ctx.current_indent()) { break; } // A continuation line that contains an implicit mapping-key // indicator (": " / ":\n") starts a new entry, not key text. { auto scan = continuation; while (scan != end && *scan != '\n' && *scan != '\r') { if (*scan == ':') { auto after_colon = scan + 1; const bool tight_key_colon = (scan == continuation) || (*(scan - 1) != ' ' && *(scan - 1) != '\t'); if (tight_key_colon && (after_colon == end || *after_colon == ' ' || *after_colon == '\t' || *after_colon == '\n' || *after_colon == '\r')) { break; } } ++scan; } if (scan != end && *scan == ':') { break; } } } if (!key.empty() && key.back() != ' ') { key.push_back(' '); } it = continuation; continue; } if (in_flow && (c == ',' || c == ']' || c == '}')) break; if (c == '#') { if (key.empty() || key.back() == ' ' || key.back() == '\t') break; } key.push_back(c); ++it; } // Trim trailing whitespace from key while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) { key.pop_back(); } // Empty keys are valid YAML (":" in block or flow mappings). } if (bool(ctx.error)) return false; // Implicit mapping keys must be single-line in both block and flow styles. if (quoted_key_spans_lines && !in_flow) { ctx.error = error_code::syntax_error; return false; } // Store anchor on key if present if (has_key_anchor) { ctx.anchors[std::move(key_anchor_name)] = {key_start, &*it, ctx.current_indent()}; } return true; } } // namespace yaml // String types template struct from { template static void op(auto&& value, is_context auto&& ctx, It&& it, auto end) noexcept { if (bool(ctx.error)) [[unlikely]] return; yaml::node_preamble_state preamble{}; if (yaml::parse_node_preamble( value, ctx, it, end, preamble, [](yaml::yaml_tag tag) { return yaml::tag_valid_for_string(tag); })) return; std::string str; const auto style = yaml::detect_scalar_style(*it); switch (style) { case yaml::scalar_style::double_quoted: yaml::parse_double_quoted_string(str, ctx, it, end); break; case yaml::scalar_style::single_quoted: yaml::parse_single_quoted_string(str, ctx, it, end); break; case yaml::scalar_style::literal_block: case yaml::scalar_style::folded_block: // For same-line mapping/sequence values, parsing context is typically pushed // one column past the key/item indicator; block scalar indentation is relative // to the parent line indent. yaml::parse_block_scalar(str, ctx, it, end, ctx.current_indent() > 0 ? (ctx.current_indent() - 1) : ctx.current_indent()); break; case yaml::scalar_style::plain: // In block context with known indent, use multiline parsing to support // plain scalars that span multiple lines (folding behavior) if constexpr (!yaml::check_flow_context(Opts)) { if (ctx.current_indent() >= 0) { yaml::parse_plain_scalar_multiline(str, ctx, it, end, ctx.current_indent()); } else { // Top-level plain scalars may continue on same-indent or indented lines. yaml::parse_plain_scalar_multiline(str, ctx, it, end, int32_t(0)); } } else { yaml::parse_plain_scalar(str, ctx, it, end, true); } break; } if (!bool(ctx.error)) { value = std::move(str); yaml::finalize_node_anchor(preamble.node_props, ctx, it); } } }; // Boolean types template struct from { template static void op(auto&& value, is_context auto&& ctx, It&& it, auto end) noexcept { if (bool(ctx.error)) [[unlikely]] return; yaml::node_preamble_state preamble{}; if (yaml::parse_node_preamble(value, ctx, it, end, preamble, [](yaml::yaml_tag tag) { return yaml::tag_valid_for_bool(tag); })) return; // Parse as plain scalar first std::string str; yaml::parse_plain_scalar(str, ctx, it, end, yaml::check_flow_context(Opts)); if (str == "true" || str == "True" || str == "TRUE" || str == "yes" || str == "Yes" || str == "YES" || str == "on" || str == "On" || str == "ON") { value = true; } else if (str == "false" || str == "False" || str == "FALSE" || str == "no" || str == "No" || str == "NO" || str == "off" || str == "Off" || str == "OFF") { value = false; } else { ctx.error = error_code::expected_true_or_false; } yaml::finalize_node_anchor(preamble.node_props, ctx, it); } }; // Numeric types template struct from { template static void op(auto&& value, is_context auto&& ctx, It&& it, auto end) noexcept { if (bool(ctx.error)) [[unlikely]] return; yaml::node_preamble_state preamble{}; if (yaml::parse_node_preamble(value, ctx, it, end, preamble, [](yaml::yaml_tag tag) { if constexpr (std::floating_point>) { return yaml::tag_valid_for_float(tag); } else { return yaml::tag_valid_for_int(tag); } })) return; auto finalize = [&] { yaml::finalize_node_anchor(preamble.node_props, ctx, it); }; // Check for special float values if constexpr (std::floating_point>) { auto start = it; // Check for .inf, .nan, -.inf, etc. if (*it == '.') { ++it; if (it + 3 <= end && (std::string_view(it, 3) == "inf" || std::string_view(it, 3) == "Inf" || std::string_view(it, 3) == "INF")) { value = std::numeric_limits>::infinity(); it += 3; finalize(); return; } if (it + 3 <= end && (std::string_view(it, 3) == "nan" || std::string_view(it, 3) == "NaN" || std::string_view(it, 3) == "NAN")) { value = std::numeric_limits>::quiet_NaN(); it += 3; finalize(); return; } it = start; } if (*it == '-' || *it == '+') { const char sign = *it; ++it; if (it != end && *it == '.') { ++it; if (it + 3 <= end && (std::string_view(it, 3) == "inf" || std::string_view(it, 3) == "Inf" || std::string_view(it, 3) == "INF")) { if (sign == '-') { value = -std::numeric_limits>::infinity(); } else { value = std::numeric_limits>::infinity(); } it += 3; finalize(); return; } } it = start; } } // Parse as plain scalar and convert auto start = it; // Find end of number - use same rules as plain scalar for colons while (it != end) { const char c = *it; if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || c == ']' || c == '}' || c == '#') { break; } // Colon only ends number if followed by space/tab/newline/end (YAML mapping indicator) if (c == ':') { if ((it + 1) == end || *(it + 1) == ' ' || *(it + 1) == '\t' || *(it + 1) == '\n' || *(it + 1) == '\r') { break; } } ++it; } const std::string_view num_str(&*start, static_cast(it - start)); if (num_str.empty()) { ctx.error = error_code::parse_number_failure; return; } // Handle hex, octal, binary if constexpr (std::integral>) { if (num_str.size() > 2 && num_str[0] == '0') { int base = 10; size_t offset = 0; if (num_str[1] == 'x' || num_str[1] == 'X') { base = 16; offset = 2; } else if (num_str[1] == 'o' || num_str[1] == 'O') { base = 8; offset = 2; } else if (num_str[1] == 'b' || num_str[1] == 'B') { base = 2; offset = 2; } if (base != 10) { const auto digits = num_str.substr(offset); const bool has_underscores = (digits.find('_') != std::string_view::npos); std::string clean_storage; std::string_view clean_view; if (has_underscores) { clean_storage.reserve(digits.size()); for (char c : digits) { if (c != '_') { clean_storage.push_back(c); } } clean_view = clean_storage; } else { clean_view = digits; } auto [ptr, ec] = std::from_chars(clean_view.data(), clean_view.data() + clean_view.size(), value, base); if (ec != std::errc{}) { ctx.error = error_code::parse_number_failure; } finalize(); return; } } } else if constexpr (std::floating_point>) { bool negative = false; std::string_view digits = num_str; if (!digits.empty() && (digits.front() == '+' || digits.front() == '-')) { negative = (digits.front() == '-'); digits.remove_prefix(1); } if (digits.size() > 2 && digits[0] == '0') { int base = 10; size_t offset = 0; if (digits[1] == 'x' || digits[1] == 'X') { base = 16; offset = 2; } else if (digits[1] == 'o' || digits[1] == 'O') { base = 8; offset = 2; } else if (digits[1] == 'b' || digits[1] == 'B') { base = 2; offset = 2; } if (base != 10) { auto raw_digits = digits.substr(offset); const bool has_underscores = (raw_digits.find('_') != std::string_view::npos); std::string clean_storage; std::string_view clean_view; if (has_underscores) { clean_storage.reserve(raw_digits.size()); for (char c : raw_digits) { if (c != '_') { clean_storage.push_back(c); } } clean_view = clean_storage; } else { clean_view = raw_digits; } uint64_t parsed_int{}; auto [ptr, ec] = std::from_chars(clean_view.data(), clean_view.data() + clean_view.size(), parsed_int, base); if (ec != std::errc{} || ptr != (clean_view.data() + clean_view.size())) { ctx.error = error_code::parse_number_failure; finalize(); return; } const auto as_float = static_cast>(parsed_int); value = negative ? -as_float : as_float; finalize(); return; } } } // Standard number parsing - only allocate if underscores present const bool has_underscores = (num_str.find('_') != std::string_view::npos); std::string clean_storage; std::string_view clean_view; if (has_underscores) { clean_storage.reserve(num_str.size()); for (char c : num_str) { if (c != '_') { clean_storage.push_back(c); } } clean_view = clean_storage; } else { clean_view = num_str; } // YAML allows leading '+' for positive numbers, but C++ from_chars doesn't // Strip leading '+' if present auto parse_view = clean_view; if (!parse_view.empty() && parse_view.front() == '+') { parse_view.remove_prefix(1); if (parse_view.empty()) { ctx.error = error_code::parse_number_failure; return; } } if constexpr (std::floating_point>) { auto result = glz::fast_float::from_chars(parse_view.data(), parse_view.data() + parse_view.size(), value); // Check both for errors and that all input was consumed (e.g., "12:30" is not a valid number) if (result.ec != std::errc{} || result.ptr != parse_view.data() + parse_view.size()) { ctx.error = error_code::parse_number_failure; } } else { auto [ptr, ec] = std::from_chars(parse_view.data(), parse_view.data() + parse_view.size(), value); // Check both for errors and that all input was consumed if (ec != std::errc{} || ptr != parse_view.data() + parse_view.size()) { ctx.error = error_code::parse_number_failure; } } finalize(); } }; // Nullable types: std::optional, std::unique_ptr, std::shared_ptr, raw pointers template struct from { template static void op(auto&& value, is_context auto&& ctx, It&& it, auto end) noexcept { if (bool(ctx.error)) [[unlikely]] return; // Save position before consuming whitespace so we can restore it // if the speculative null check fails. The indentation whitespace is // structurally significant for block-style inner values (sequences, mappings). auto before_ws = it; yaml::skip_inline_ws(it, end); if (it == end) { value = {}; return; } // Check for tag - but don't consume it yet if it's not a null tag auto tag_start = it; const auto tag = yaml::parse_yaml_tag(it, end, ctx); if (tag == yaml::yaml_tag::unknown) [[unlikely]] { ctx.error = error_code::syntax_error; return; } // If it's explicitly a null tag, set to null if (tag == yaml::yaml_tag::null_tag) { yaml::skip_inline_ws(it, end); // Skip the null value if present if (it != end && !yaml::flow_context_end_table[static_cast(*it)]) { std::string str; yaml::parse_plain_scalar(str, ctx, it, end, yaml::check_flow_context(Opts)); } value = {}; return; } // If no tag or a non-null tag, reset and check value if (tag == yaml::yaml_tag::none) { it = tag_start; } yaml::skip_inline_ws(it, end); // Handle alias for the whole nullable value if (yaml::handle_alias(value, ctx, it, end)) return; // Anchors (&name) pass through to the inner type handler // Check for null value (without tag) if (tag == yaml::yaml_tag::none) { std::string str; yaml::parse_plain_scalar(str, ctx, it, end, yaml::check_flow_context(Opts)); if (yaml::is_yaml_null(str)) { value = {}; return; } // Not null - reset to before whitespace consumption so the inner // parser can measure indentation correctly for block-style values. it = before_ws; } if (!value) { if (!nullable_emplace(value, ctx)) { return; } } using V = std::remove_cvref_t; from::template op(*value, ctx, it, end); } }; // Always-null literal types (std::nullptr_t, std::monostate, std::nullopt_t) template struct from { template static void op(auto&&, is_context auto&& ctx, It&& it, auto end) noexcept { if (bool(ctx.error)) [[unlikely]] return; yaml::skip_inline_ws(it, end); if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } // Check for null values: null, Null, NULL, ~ if (*it == '~') { ++it; return; } // Parse as plain scalar and check if it's a null keyword auto start = it; while (it != end && !yaml::plain_scalar_end_table[static_cast(*it)]) { ++it; } std::string_view str{start, static_cast(it - start)}; if (!yaml::is_yaml_null(str)) { ctx.error = error_code::syntax_error; } } }; // Enum with glz::meta - reads string representation template requires(is_named_enum) struct from { template static void op(auto& value, is_context auto&& ctx, It&& it, auto end) noexcept { if (bool(ctx.error)) [[unlikely]] return; yaml::node_preamble_state preamble{}; if (yaml::parse_node_preamble(value, ctx, it, end, preamble, [](yaml::yaml_tag tag) { return yaml::tag_valid_for_string(tag); })) return; // Parse as string (quoted or plain) std::string str; const auto style = yaml::detect_scalar_style(*it); if (style == yaml::scalar_style::double_quoted) { yaml::parse_double_quoted_string(str, ctx, it, end); } else if (style == yaml::scalar_style::single_quoted) { yaml::parse_single_quoted_string(str, ctx, it, end); } else { yaml::parse_plain_scalar(str, ctx, it, end, yaml::check_flow_context(Opts)); } if (bool(ctx.error)) [[unlikely]] return; constexpr auto N = reflect::size; if constexpr (N == 1) { static constexpr auto key = glz::get<0>(reflect::keys); if (str == key) { value = glz::get<0>(reflect::values); } else { ctx.error = error_code::unexpected_enum; } } else { static constexpr auto HashInfo = hash_info; const auto index = decode_hash_with_size::op( str.data(), str.data() + str.size(), str.size()); if (index >= N) [[unlikely]] { ctx.error = error_code::unexpected_enum; return; } visit( [&]() { static constexpr auto key = glz::get(reflect::keys); if (str == key) [[likely]] { value = glz::get(reflect::values); } else { ctx.error = error_code::unexpected_enum; } }, index); } yaml::finalize_node_anchor(preamble.node_props, ctx, it); } }; // Raw enum (without glz::meta) - reads as underlying numeric type template requires(std::is_enum_v && !glaze_enum_t && !meta_keys && !custom_read) struct from { template static void op(auto& value, is_context auto&& ctx, It&& it, auto end) noexcept { std::underlying_type_t> x{}; from::template op(x, ctx, it, end); value = static_cast>(x); } }; namespace yaml { // Parse flow sequence [item, item, ...] template inline void parse_flow_sequence(T&& value, Ctx& ctx, It& it, End end) { using V = std::remove_cvref_t; using value_type = typename V::value_type; struct sequence_container_adapter { size_t index = 0; inline bool fixed_capacity_reached(const V& container) const noexcept { if constexpr (!resizable) { return index >= container.size(); } else { (void)container; return false; } } inline void commit_fixed_slot() noexcept { ++index; } inline void append(V& container, value_type&& element) noexcept { if constexpr (emplace_backable) { container.emplace_back(std::move(element)); } else if constexpr (emplaceable) { container.emplace(std::move(element)); } } }; if (it == end || *it != '[') [[unlikely]] { ctx.error = error_code::syntax_error; return; } ++it; // Skip '[' // Skip whitespace and newlines after opening bracket (YAML allows multi-line flow sequences) skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; // Handle empty array if (it != end && *it == ']') { ++it; validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if constexpr (!yaml::check_flow_context(Opts)) { if (ctx.current_indent() < 0) { validate_root_flow_tail_after_close(ctx, it, end); } } return; } bool just_saw_comma = false; sequence_container_adapter adapter{}; if constexpr (emplace_backable || emplaceable) { // Dynamic containers append items (vector-like and set-like) while (it != end) { skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } if (*it == ']') { if (just_saw_comma) { ++it; validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if constexpr (!yaml::check_flow_context(Opts)) { if (ctx.current_indent() < 0) { validate_root_flow_tail_after_close(ctx, it, end); } } return; } ctx.error = error_code::syntax_error; return; } if (*it == ',' || *it == '#') { ctx.error = error_code::syntax_error; return; } just_saw_comma = false; value_type element{}; from::template op()>(element, ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; bool saw_line_break_before_separator = false; skip_flow_ws_and_newlines(ctx, it, end, &saw_line_break_before_separator); if (bool(ctx.error)) [[unlikely]] return; if (it != end && *it == ':') { // In flow sequences, implicit key-value pairs must be on a single line. if (saw_line_break_before_separator) { ctx.error = error_code::syntax_error; return; } if constexpr (std::same_as, glz::generic>) { glz::generic key_node = std::move(element); ++it; skip_inline_ws(it, end); glz::generic mapped{}; from::template op()>(mapped, ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; std::string key; if (key_node.is_null()) { key.clear(); } else if (auto* s = key_node.template get_if()) { key = *s; } else { (void)glz::write_json(key_node, key); } glz::generic pair{}; pair[key] = std::move(mapped); element = std::move(pair); skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; } else { ctx.error = error_code::syntax_error; return; } } adapter.append(value, std::move(element)); if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } if (*it == ']') { ++it; validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if constexpr (!yaml::check_flow_context(Opts)) { if (ctx.current_indent() < 0) { validate_root_flow_tail_after_close(ctx, it, end); } } return; } else if (*it == ',') { ++it; just_saw_comma = true; skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; } else { ctx.error = error_code::syntax_error; return; } } } else if constexpr (!resizable) { // Fixed-size containers (std::array) while (it != end && !adapter.fixed_capacity_reached(value)) { skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } if (*it == ']') { if (just_saw_comma) { ++it; validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if constexpr (!yaml::check_flow_context(Opts)) { if (ctx.current_indent() < 0) { validate_root_flow_tail_after_close(ctx, it, end); } } return; } ctx.error = error_code::syntax_error; return; } if (*it == ',' || *it == '#') { ctx.error = error_code::syntax_error; return; } just_saw_comma = false; from::template op()>(value[adapter.index], ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; adapter.commit_fixed_slot(); skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } if (*it == ']') { ++it; validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if constexpr (!yaml::check_flow_context(Opts)) { if (ctx.current_indent() < 0) { validate_root_flow_tail_after_close(ctx, it, end); } } return; } else if (*it == ',') { ++it; just_saw_comma = true; skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; } else { ctx.error = error_code::syntax_error; return; } } // If we exited because the fixed-size container is full but haven't seen ']', // skip to the closing bracket to consume overflow elements. int bracket_depth = 1; while (it != end && bracket_depth > 0) { if (*it == '[') { ++bracket_depth; } else if (*it == ']') { --bracket_depth; } ++it; } } else { ctx.error = error_code::syntax_error; return; } } // Parse flow mapping {key: value, ...} template inline void parse_flow_mapping(T&& value, Ctx& ctx, It& it, End end) { using U = std::remove_cvref_t; static constexpr auto N = reflect::size; static constexpr auto HashInfo = hash_info; static constexpr sv variant_tag = Tag.sv(); decltype(auto) fields = [&]() -> decltype(auto) { if constexpr (Opts.error_on_missing_keys) { return bit_array{}; } else { return nullptr; } }(); if (it == end || *it != '{') [[unlikely]] { ctx.error = error_code::syntax_error; return; } ++it; // Skip '{' skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; // Handle empty mapping if (it != end && *it == '}') { ++it; } else { while (it != end) { skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if (it != end && *it == '}') { ++it; validate_flow_node_adjacent_tail(ctx, it, end); break; } // Parse key using scratch buffer to avoid allocation ctx.scratch.clear(); if (!parse_yaml_key(ctx.scratch, ctx, it, end, true)) { return; } // Separation between flow key and ':' may include comments/newlines. // In flow context, newlines are allowed between key and ':'. skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; // Expect colon if (it == end || *it != ':') { ctx.error = error_code::syntax_error; return; } ++it; skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; // Look up key and parse value const auto index = decode_hash_with_size::op( ctx.scratch.data(), ctx.scratch.data() + ctx.scratch.size(), ctx.scratch.size()); const bool key_matches = index < N && std::string_view{ctx.scratch} == reflect::keys[index]; if (key_matches) [[likely]] { if constexpr (Opts.error_on_missing_keys) { fields[index] = true; } visit( [&]() { if (I == index) { decltype(auto) member = [&]() -> decltype(auto) { if constexpr (reflectable) { return get(to_tie(value)); } else { return get_member(value, get(reflect::values)); } }(); using member_type = std::decay_t; from::template op()>(member, ctx, it, end); } return !bool(ctx.error); }, index); } else if constexpr (not variant_tag.empty()) { // A tagged variant's discriminator key is "unknown" to the resolved struct (unless // the struct declares it as a field). The variant resolver already consumed it to // choose this alternative, so it is skipped rather than rejected. if constexpr (Opts.error_on_unknown_keys) { if (std::string_view{ctx.scratch} != variant_tag) { ctx.error = error_code::unknown_key; return; } } skip_yaml_value(ctx, it, end, 0, true); } else if constexpr (Opts.error_on_unknown_keys) { ctx.error = error_code::unknown_key; return; } else { // else used to fix MSVC unreachable code warning skip_yaml_value(ctx, it, end, 0, true); } if (bool(ctx.error)) [[unlikely]] return; // In flow context, newlines are treated as whitespace (YAML spec). // Use skip_flow_ws_and_newlines so that a newline between a value // and its separator (comma or closing brace) is accepted. skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if (it != end && *it == '}') { ++it; validate_flow_node_adjacent_tail(ctx, it, end); break; } else if (it != end && *it == ',') { ++it; } else { ctx.error = error_code::syntax_error; return; } } } if constexpr (Opts.error_on_missing_keys) { if (bool(ctx.error)) return; constexpr auto req_fields = required_fields(); if ((req_fields & fields) != req_fields) { for (size_t i = 0; i < N; ++i) { if (not fields[i] && req_fields[i]) { ctx.custom_error_message = reflect::keys[i]; break; } } ctx.error = error_code::missing_key; } } } // Detects a plain "key: value" mapping indicator in an inline block-map // value segment, while ignoring quoted strings and nested flow collections. template inline bool inline_value_has_plain_mapping_indicator(It pos, End end) noexcept { int flow_depth = 0; while (pos != end) { const char c = *pos; if (c == '\n' || c == '\r' || c == '#') return false; if (c == '"' || c == '\'') { const char quote = c; ++pos; while (pos != end && *pos != quote) { if (*pos == '\\' && quote == '"') { ++pos; if (pos != end) ++pos; } else { ++pos; } } if (pos != end) ++pos; continue; } if (c == '[' || c == '{') { ++flow_depth; ++pos; continue; } if ((c == ']' || c == '}') && flow_depth > 0) { --flow_depth; ++pos; continue; } if (c == ':' && flow_depth == 0) { const auto next = pos + 1; return (next == end) || *next == ' ' || *next == '\t' || *next == '\n' || *next == '\r'; } ++pos; } return false; } // Parse block sequence // - item1 // - item2 template inline void parse_block_sequence(T&& value, Ctx& ctx, It& it, End end, int32_t sequence_indent) { using V = std::remove_cvref_t; using value_type = typename V::value_type; struct sequence_container_adapter { size_t index = 0; inline bool fixed_capacity_reached(const V& container) const noexcept { if constexpr (!resizable) { return index >= container.size(); } else { (void)container; return false; } } inline void commit_fixed_slot() noexcept { ++index; } inline void append(V& container, value_type&& element) noexcept { if constexpr (emplace_backable) { container.emplace_back(std::move(element)); } else if constexpr (emplaceable) { container.emplace(std::move(element)); } } }; sequence_container_adapter adapter{}; // Track if this is the first item (for handling whitespace-consumed case) bool first_item = true; bool has_pending_item_anchor = false; std::string pending_item_anchor_name{}; int32_t pending_item_anchor_indent = sequence_indent; while (it != end) { // For fixed-size arrays (e.g. std::array), stop when we've filled all elements. // Exclude emplaceable types (e.g. std::set) which are not resizable but grow dynamically. if constexpr (!resizable && !emplaceable) { if (adapter.fixed_capacity_reached(value)) { return; } } // Skip blank lines and comments, track indent int32_t line_indent = 0; int32_t dash_col = -1; // actual column of the '-' on the line auto line_start = it; while (it != end) { if (*it == '#') { skip_comment(it, end); skip_newline(it, end); line_start = it; line_indent = 0; } else if (*it == '\n' || *it == '\r') { skip_newline(it, end); line_start = it; line_indent = 0; } else if (*it == ' ') { line_start = it; line_indent = 0; while (it != end && *it == ' ') { ++line_indent; ++it; } bool saw_tab = false; while (it != end && (*it == ' ' || *it == '\t')) { saw_tab = saw_tab || (*it == '\t'); ++it; } if (it == end || *it == '\n' || *it == '\r' || *it == '#') { // Blank or comment line - continue to next line if (it != end && *it == '#') { skip_comment(it, end); } line_indent = 0; continue; } if (saw_tab) { ctx.error = error_code::syntax_error; return; } break; // Found content } else if (*it == '\t') { line_start = it; while (it != end && (*it == ' ' || *it == '\t')) ++it; if (it == end || *it == '\n' || *it == '\r' || *it == '#') { if (it != end && *it == '#') { skip_comment(it, end); } line_indent = 0; continue; } ctx.error = error_code::syntax_error; return; } else { // At content (no leading space on this line) // If first item and at '-', whitespace was consumed by caller; // treat as having correct indentation if (first_item && *it == '-') { line_indent = sequence_indent; if constexpr (std::is_pointer_v>) { if (ctx.stream_begin && it > ctx.stream_begin) { auto probe = it; int32_t leading_ws = 0; bool saw_tab = false; while (probe > ctx.stream_begin && (*(probe - 1) == ' ' || *(probe - 1) == '\t')) { --probe; ++leading_ws; saw_tab = saw_tab || (*probe == '\t'); } if (probe == ctx.stream_begin || *(probe - 1) == '\n' || *(probe - 1) == '\r') { if (saw_tab) { ctx.error = error_code::syntax_error; return; } line_indent = (std::max)(line_indent, leading_ws); } } } } break; } } if (it == end) break; // Check for document end marker if (at_document_end(it, end)) break; // Check for dedent if (line_indent < sequence_indent) { it = line_start; return; } if (*it == '&') { // Standalone anchor lines before "- item" are only supported for // indentless mapping values. At top-level block sequences these are // malformed in yaml-test-suite cases. if (!ctx.allow_indentless_sequence) { ctx.error = error_code::syntax_error; return; } ++it; auto pending_name = parse_anchor_name(it, end); if (pending_name.empty()) { ctx.error = error_code::syntax_error; return; } skip_inline_ws(it, end); if (it != end && *it == '#') { skip_comment(it, end); } has_pending_item_anchor = true; pending_item_anchor_name = std::string(pending_name); pending_item_anchor_indent = line_indent; if (it == end || (*it != '\n' && *it != '\r')) { ctx.error = error_code::syntax_error; return; } skip_newline(it, end); continue; } // Expect dash for sequence item if (*it != '-') { it = line_start; return; } // Compute the actual column of this sequence dash. // This is needed by plain-scalar multiline folding to distinguish // sibling "- item" entries from deeper "- " continuation content. dash_col = line_indent; if constexpr (std::is_pointer_v>) { if (ctx.stream_begin) { auto p = &*it; dash_col = 0; while (p > ctx.stream_begin && *(p - 1) != '\n' && *(p - 1) != '\r') { --p; ++dash_col; } } } ++it; // Skip '-' // Check for valid sequence item indicator (- followed by space or newline) if (it != end && !yaml::whitespace_or_line_end_table[static_cast(*it)]) { // Not a sequence item (could be a number like -5) it = line_start; return; } bool saw_tab_after_dash = false; while (it != end && (*it == ' ' || *it == '\t')) { saw_tab_after_dash = saw_tab_after_dash || (*it == '\t'); ++it; } // A tab-separated nested "- " marker is not valid block indentation. if (saw_tab_after_dash && it != end && *it == '-') { const auto after_dash = it + 1; if (after_dash == end || yaml::whitespace_or_line_end_table[static_cast(*after_dash)]) { ctx.error = error_code::syntax_error; return; } } // Get reference to element to parse into auto parse_element = [&](auto& element) { const char* element_start = (it != end) ? &*it : nullptr; // Check what follows if (it != end && !yaml::line_end_or_comment_table[static_cast(*it)]) { // Content on same line as dash - set indent to one less than content column // This allows nested block mappings to continue parsing keys at the content indent // For "- key: val", if dash is at column 0, content is at column 2 // We set parent indent to 1 so keys at column 2 pass the "indent > parent" check if (!ctx.push_indent(line_indent + 1)) [[unlikely]] return; const bool prev_sequence_item_value_context = ctx.sequence_item_value_context; const int32_t prev_sequence_dash_indent = ctx.sequence_dash_indent; ctx.sequence_item_value_context = true; ctx.sequence_dash_indent = dash_col; from::template op(element, ctx, it, end); ctx.sequence_dash_indent = prev_sequence_dash_indent; ctx.sequence_item_value_context = prev_sequence_item_value_context; ctx.pop_indent(); } else { // Value on next line - nested block skip_ws_and_comment(it, end); skip_newline(it, end); // Get indent of nested content auto nested_start = it; int32_t nested_indent = measure_indent(it, end, ctx); if (bool(ctx.error)) [[unlikely]] return; it = nested_start; // Content is nested only if indented more than the current line. // When line_indent is negative (root level), use 0 as the baseline. // This prevents content at column 0 from being treated as nested. const int32_t effective_line_indent = (line_indent < 0) ? 0 : line_indent; if (nested_indent > effective_line_indent) { // Save and set indent for nested parsing // Set parent indent to one less than content indent so items at // content indent pass the "indent > parent" check and continue parsing if (!ctx.push_indent(nested_indent - 1)) [[unlikely]] return; const bool prev_sequence_item_value_context = ctx.sequence_item_value_context; const int32_t prev_sequence_dash_indent = ctx.sequence_dash_indent; ctx.sequence_item_value_context = true; ctx.sequence_dash_indent = dash_col; from::template op(element, ctx, it, end); ctx.sequence_dash_indent = prev_sequence_dash_indent; ctx.sequence_item_value_context = prev_sequence_item_value_context; ctx.pop_indent(); } // else: empty element (default constructed) } if (has_pending_item_anchor && !bool(ctx.error)) { const char* element_end = (it != end) ? &*it : element_start; ctx.anchors[std::move(pending_item_anchor_name)] = {element_start, element_end, pending_item_anchor_indent}; has_pending_item_anchor = false; } }; if constexpr (emplace_backable) { auto& element = value.emplace_back(); parse_element(element); } else if constexpr (emplaceable) { value_type element{}; parse_element(element); if (!bool(ctx.error)) { adapter.append(value, std::move(element)); } } else if constexpr (!resizable) { parse_element(value[adapter.index]); adapter.commit_fixed_slot(); } else { ctx.error = error_code::syntax_error; return; } first_item = false; if (bool(ctx.error)) [[unlikely]] return; // Note: after parsing nested block content, iterator may already be // at the start of the next line. The outer loop will handle it. } } // Detect whether there is nested block content after a `key:` with no same-line value. // Called with `it` positioned after the colon and any trailing inline whitespace. // Peeks ahead past blank lines and comment-only lines to find the first content line. // Returns the indent of that content if it's nested (> effective line_indent), else -1. // On return, `it` is positioned right after the key line's newline. // If return >= 0, caller should call skip_to_content() to advance to the content. // // High-level state machine: // 1) Validate that we are at end-of-key line and move to the next line. // 2) Scan forward to first real content line, skipping blank/comment lines. // Also skip "property-only" lines (YAML node properties with no value yet). // 3) Measure indent and validate tab/structural edge cases. // 4) Return nested indent when content is truly nested, otherwise -1. template int32_t detect_nested_value_indent(Ctx& ctx, It& it, End end, int32_t line_indent) { skip_ws_and_comment(it, end); if (it == end || (*it != '\n' && *it != '\r')) { return -1; } skip_newline(it, end); auto content_start = it; // Peek ahead to find the first content line (skip blank/comment lines) auto peek = it; while (peek != end) { if (*peek == '\n' || *peek == '\r') { skip_newline(peek, end); } else if (*peek == '#') { skip_comment(peek, end); } else if (*peek == ' ') { auto line_start = peek; while (peek != end && *peek == ' ') ++peek; if (peek == end || *peek == '\n' || *peek == '\r') { continue; // blank line with trailing spaces } if (*peek == '#') { skip_comment(peek, end); continue; // comment-only line } // YAML allows node properties to appear on standalone lines before the // actual node value, for example: // key: // !tag // &anchor // actual: value // This loop recognizes chains of properties (`!tag`, `&anchor`, optional // inline spaces/comments) and treats those lines as non-content so they do // not incorrectly define nested indentation. { auto prop = peek; bool saw_property = false; while (prop != end) { if (*prop == '!') { const auto tag = parse_yaml_tag(prop, end, ctx); if (tag == yaml_tag::unknown) break; saw_property = true; skip_inline_ws(prop, end); continue; } if (*prop == '&') { ++prop; auto aname = parse_anchor_name(prop, end); if (aname.empty()) break; saw_property = true; skip_inline_ws(prop, end); continue; } break; } if (saw_property) { auto tail = prop; skip_inline_ws(tail, end); if (tail == end || *tail == '\n' || *tail == '\r' || *tail == '#') { if (tail != end && *tail == '#') { skip_comment(tail, end); } if (tail != end && (*tail == '\n' || *tail == '\r')) { skip_newline(tail, end); } peek = tail; continue; } } } peek = line_start; // content found, restore to measure properly break; } else { break; // content at column 0 } } // Measure indent of the content line int32_t content_indent = measure_indent(peek, end, ctx); if (bool(ctx.error)) [[unlikely]] return -1; // Tabs in indentation-like positions are only allowed when they are plain // scalar content. Structural lines (mapping keys/entries) remain errors. if (peek != end && *peek == '\t') { auto probe = peek; while (probe != end && (*probe == ' ' || *probe == '\t')) ++probe; if (probe != end && *probe != '\n' && *probe != '\r' && *probe != '#') { const bool looks_sequence_entry = (*probe == '-') && ((probe + 1) == end || yaml::whitespace_or_line_end_table[static_cast(*(probe + 1))]); const bool looks_explicit_entry = (*probe == '?' || *probe == ':') && ((probe + 1) == end || yaml::whitespace_or_line_end_table[static_cast(*(probe + 1))]); bool looks_mapping_key = false; auto scan = probe; while (scan != end && *scan != '\n' && *scan != '\r') { if (*scan == ':') { const auto after = scan + 1; if (after == end || yaml::whitespace_or_line_end_table[static_cast(*after)]) { looks_mapping_key = true; break; } } ++scan; } if (looks_sequence_entry || looks_explicit_entry || looks_mapping_key) { ctx.error = error_code::syntax_error; return -1; } } } const int32_t effective_line_indent = (line_indent < 0) ? 0 : line_indent; if (content_indent > effective_line_indent && peek != end && *peek != '\n' && *peek != '\r') { it = content_start; return content_indent; } // YAML allows "indentless sequences" as mapping values: // key: // - item if (content_indent == effective_line_indent && peek != end && *peek == '-') { const auto after_dash = peek + 1; if (after_dash == end || yaml::whitespace_or_line_end_table[static_cast(*after_dash)]) { it = content_start; return content_indent; } } it = content_start; return -1; } // Skip blank lines and comment-only lines. // Leaves `it` at the start of indentation whitespace of the first content line. template void skip_to_content(It& it, End end) { while (it != end) { if (*it == '\n' || *it == '\r') { skip_newline(it, end); } else if (*it == '#') { skip_comment(it, end); } else if (*it == ' ') { auto line_start = it; while (it != end && *it == ' ') ++it; if (it == end || *it == '\n' || *it == '\r') { continue; // blank line with trailing spaces } if (*it == '#') { skip_comment(it, end); continue; // comment-only line } it = line_start; // content found, restore to start of indent return; } else { return; // content at column 0 } } } // Skip the value of an unknown block-mapping entry, whether it is inline (on the key's line) // or begins on a following, more-indented line (a nested block sequence or mapping). // `key_indent` is the column of the entry's key; deeper lines belong to the value. template inline void skip_unknown_block_value(Ctx& ctx, It& it, End end, int32_t key_indent) noexcept { if (it != end && !yaml::line_end_or_comment_table[static_cast(*it)]) { skip_yaml_value(ctx, it, end, key_indent, false); } else { const int32_t nested_indent = detect_nested_value_indent(ctx, it, end, key_indent); if (nested_indent >= 0) { skip_to_content(it, end); skip_yaml_value(ctx, it, end, key_indent, false); } } } // Shared loop for block mapping parsing. // Handles blank/comment skipping, indent detection, dedent, and trailing whitespace. // process_entry(ctx, it, end, line_indent) should parse key+colon+value and return // true to continue or false to stop. // // mapping_indent >= 0: caller knows the key indent (struct case, first key may be mid-line) // mapping_indent < 0: discover from first key (map case) template void parse_block_mapping_loop(Ctx& ctx, It& it, End end, int32_t mapping_indent, ProcessEntry&& process_entry) { const int32_t parent_indent = ctx.current_indent(); const bool discover_indent = (mapping_indent < 0); bool first_key = !discover_indent; bool discovered_first_key_mid_line = false; int32_t discovered_first_key_visual_indent = 0; while (it != end) { auto line_start = it; int32_t line_indent = first_key ? mapping_indent : 0; // Skip blank lines and comments, measure indent while (it != end) { if (*it == '#') { skip_comment(it, end); skip_newline(it, end); first_key = false; line_indent = 0; } else if (*it == '\n' || *it == '\r') { skip_newline(it, end); first_key = false; line_indent = 0; } else if (*it == ' ') { line_start = it; line_indent = 0; while (it != end && *it == ' ') { ++line_indent; ++it; } bool saw_tab = false; while (it != end && (*it == ' ' || *it == '\t')) { saw_tab = saw_tab || (*it == '\t'); ++it; } if (it == end || *it == '\n' || *it == '\r' || *it == '#') { first_key = false; line_indent = 0; continue; // Blank or comment line } if (saw_tab) { ctx.error = error_code::syntax_error; return; } // Early dedent check if (mapping_indent >= 0 && line_indent < mapping_indent) { it = line_start; return; } first_key = false; break; // Found content } else if (*it == '\t') { line_start = it; while (it != end && (*it == ' ' || *it == '\t')) ++it; if (it == end || *it == '\n' || *it == '\r' || *it == '#') { first_key = false; line_indent = 0; continue; // Blank or comment-only line with tab indentation } ctx.error = error_code::syntax_error; return; } else { line_start = it; break; // Content at column 0 } } if (it == end) break; // Document boundaries terminate the current mapping in both discovered // and fixed-indent modes. if (at_document_end(it, end) || at_document_start(it, end)) break; // Dedent checks (skip on first key for struct case) // Must check BEFORE establishing mapping_indent so the first discovered key // isn't rejected by the parent_indent check (mapping_indent is still -1). if (!first_key) { // Parent indent check for nested maps (only after mapping_indent established) if (discover_indent && mapping_indent >= 0 && parent_indent >= 0 && line_indent <= parent_indent) { it = line_start; return; } if (mapping_indent >= 0 && line_indent < mapping_indent) { it = line_start; return; } } // Establish mapping indent from first key (map case) bool established_mapping_indent_this_line = false; if (mapping_indent < 0) { mapping_indent = line_indent; established_mapping_indent_this_line = true; discovered_first_key_visual_indent = line_indent; if constexpr (std::is_pointer_v>) { if (ctx.stream_begin && line_start > ctx.stream_begin) { auto line_begin = line_start; while (line_begin > ctx.stream_begin && *(line_begin - 1) != '\n' && *(line_begin - 1) != '\r') { --line_begin; } bool seen_non_whitespace = false; int32_t visual_indent = 0; for (auto p = line_begin; p != line_start; ++p) { if (*p == '\t') { ctx.error = error_code::syntax_error; return; } if (*p != ' ') { seen_non_whitespace = true; } ++visual_indent; } discovered_first_key_mid_line = seen_non_whitespace; if (mapping_indent == 0 && visual_indent > 0) { discovered_first_key_visual_indent = visual_indent; } } } } // In discovered-indent block mappings, sibling keys must stay at the same // indent level, except when the first key started mid-line (e.g. "--- k: v"). if (discover_indent && mapping_indent >= 0 && !established_mapping_indent_this_line && !discovered_first_key_mid_line) { if (mapping_indent == 0 && discovered_first_key_visual_indent > 0) { if (line_indent > discovered_first_key_visual_indent) { ctx.error = error_code::syntax_error; return; } if (line_indent < discovered_first_key_visual_indent) { it = line_start; return; } } else if (line_indent > mapping_indent && (parent_indent < 0 || mapping_indent > 0 || (parent_indent >= 0 && mapping_indent == 0 && line_indent > (parent_indent + 1)))) { ctx.error = error_code::syntax_error; return; } } // Sequence indicator check (struct case only - map parser lets key parsing // handle '- ' which produces appropriate errors for misindented items) if (!discover_indent && *it == '-' && ((it + 1) == end || *(it + 1) == ' ' || *(it + 1) == '\t' || *(it + 1) == '\n')) { it = line_start; return; } // Skip indented comment lines if (*it == '#') { skip_comment(it, end); first_key = false; continue; } // Skip blank/empty lines after indent if (*it == '\n' || *it == '\r') { first_key = false; continue; } // While parsing an explicit mapping key node ('? key-node'), // a subsequent standalone ':' at the same indentation starts // the outer value indicator and must terminate this key-node map. // Keep same-line ': x' on the first key-node line valid. if (ctx.explicit_mapping_key_context && !established_mapping_indent_this_line && *it == ':') { const auto after_colon = it + 1; const int32_t explicit_value_indicator_indent = (parent_indent < 0) ? 0 : (parent_indent + 1); if ((after_colon == end || yaml::whitespace_or_line_end_table[static_cast(*after_colon)]) && line_indent == explicit_value_indicator_indent) { it = line_start; return; } } // Process this mapping entry (key + colon + value) int32_t effective_line_indent = line_indent; if (discover_indent && established_mapping_indent_this_line && discovered_first_key_mid_line && parent_indent >= 0 && discovered_first_key_visual_indent > effective_line_indent) { // First discovered key may begin mid-line (e.g. sequence entry "- key: value"); // pass visual key indent so same-line values compute block-scalar indentation correctly. effective_line_indent = discovered_first_key_visual_indent; } if (!process_entry(ctx, it, end, effective_line_indent)) { return; } first_key = false; if (bool(ctx.error)) [[unlikely]] return; if constexpr (yaml::check_flow_context(Opts)) { // In flow context, an implicit "key: value" pair used as a sequence entry // ends at ',', ']', or '}'. Let the enclosing flow parser consume it. auto flow_end = it; skip_flow_ws_and_newlines(ctx, flow_end, end); if (bool(ctx.error)) [[unlikely]] return; if (flow_end != end && (*flow_end == ',' || *flow_end == ']' || *flow_end == '}')) { it = flow_end; return; } } // Trailing whitespace handling (peek-ahead pattern). // After parsing, iterator may be at leading indent of next line. // Don't consume it - let the outer loop handle indent measurement. if (it != end) { if (*it == ' ' || *it == '\t') { auto peek = it; skip_inline_ws(peek, end); if (peek != end && *peek != '\n' && *peek != '\r' && *peek != '#') { continue; // At leading indent of new line } } skip_inline_ws(it, end); if (it != end && *it == '#') { if constexpr (std::is_pointer_v>) { if (it > line_start) { const char prev = *(it - 1); if (prev != ' ' && prev != '\t' && prev != '\n' && prev != '\r') { ctx.error = error_code::syntax_error; return; } } } } skip_comment(it, end); if (it != end && (*it == '\n' || *it == '\r')) { skip_newline(it, end); } } } } // Parse block mapping for struct/object types // key1: value1 // key2: value2 template inline void parse_block_mapping(T&& value, Ctx& ctx, It& it, End end, int32_t mapping_indent) { using U = std::remove_cvref_t; static constexpr auto N = reflect::size; static constexpr auto HashInfo = hash_info; decltype(auto) fields = [&]() -> decltype(auto) { if constexpr (Opts.error_on_missing_keys) { return bit_array{}; } else { return nullptr; } }(); // Clamp to >= 0 so the shared loop uses struct mode (discover_indent = false) if (mapping_indent < 0) mapping_indent = 0; parse_block_mapping_loop( ctx, it, end, mapping_indent, [&](Ctx& ctx, It& it, End end, int32_t line_indent) -> bool { // Parse key using scratch buffer to avoid allocation ctx.scratch.clear(); if (!parse_yaml_key(ctx.scratch, ctx, it, end, false)) { return false; } skip_inline_ws(it, end); // Expect colon if (it == end || *it != ':') { ctx.error = error_code::syntax_error; return false; } ++it; skip_inline_ws(it, end); // Look up key const auto index = decode_hash_with_size::op( ctx.scratch.data(), ctx.scratch.data() + ctx.scratch.size(), ctx.scratch.size()); const bool key_matches = index < N && std::string_view{ctx.scratch} == reflect::keys[index]; if (key_matches) [[likely]] { if constexpr (Opts.error_on_missing_keys) { fields[index] = true; } visit( [&]() { if (I == index) { decltype(auto) member = [&]() -> decltype(auto) { if constexpr (reflectable) { return get(to_tie(value)); } else { return get_member(value, get(reflect::values)); } }(); using member_type = std::decay_t; // Check if value is on same line or next line if (it != end && !yaml::line_end_or_comment_table[static_cast(*it)]) { if (!ctx.push_indent(line_indent + 1)) [[unlikely]] return false; from::template op(member, ctx, it, end); ctx.pop_indent(); } else { int32_t nested_indent = detect_nested_value_indent(ctx, it, end, line_indent); if (nested_indent >= 0) { skip_to_content(it, end); constexpr bool uses_discovered_block_mapping_indent = [] { if constexpr (readable_map_t || is_variant) { return true; } else if constexpr (glaze_value_t) { using unwrapped_member_type = std::decay_t(), meta_wrapper_v))>; return readable_map_t || is_variant; } else { return false; } }(); if constexpr (uses_discovered_block_mapping_indent) { if (!ctx.push_indent(nested_indent - 1)) [[unlikely]] return false; } else { if (!ctx.push_indent(nested_indent)) [[unlikely]] return false; } const bool prev_allow_indentless_sequence = ctx.allow_indentless_sequence; ctx.allow_indentless_sequence = (nested_indent <= line_indent); from::template op(member, ctx, it, end); ctx.allow_indentless_sequence = prev_allow_indentless_sequence; ctx.pop_indent(); } } } return !bool(ctx.error); }, index); } else if constexpr (not Tag.sv().empty()) { // A tagged variant's discriminator key is "unknown" to the resolved struct (unless // the struct declares it as a field). The variant resolver already consumed it to // choose this alternative, so it is skipped rather than rejected - even under // error_on_unknown_keys. The mapping's true column is on the indent stack // (established by the variant op), so this generic skip does not fold a following // sibling entry into the discriminator's inline value. if constexpr (Opts.error_on_unknown_keys) { if (std::string_view{ctx.scratch} != Tag.sv()) { ctx.error = error_code::unknown_key; return false; } } skip_unknown_block_value(ctx, it, end, line_indent); } else if constexpr (Opts.error_on_unknown_keys) { ctx.error = error_code::unknown_key; return false; } else { skip_unknown_block_value(ctx, it, end, line_indent); } return !bool(ctx.error); }); if constexpr (Opts.error_on_missing_keys) { if (bool(ctx.error)) return; constexpr auto req_fields = required_fields(); if ((req_fields & fields) != req_fields) { for (size_t i = 0; i < N; ++i) { if (not fields[i] && req_fields[i]) { ctx.custom_error_message = reflect::keys[i]; break; } } ctx.error = error_code::missing_key; } } } } // namespace yaml // Set types (std::set, std::unordered_set, etc.) template requires(readable_array_t && !emplace_backable && emplaceable) struct from { template static void op(auto&& value, is_context auto&& ctx, It&& it, auto end) { if (bool(ctx.error)) [[unlikely]] return; // Peek ahead to check for tag (don't consume indentation for block sequences) auto peek = it; yaml::node_preamble_state preamble{}; if (yaml::parse_node_preamble(value, ctx, peek, end, preamble, [](yaml::yaml_tag tag) { return yaml::tag_valid_for_seq(tag); })) { it = peek; return; } if (*peek == '[') { // Flow sequence it = peek; yaml::parse_flow_sequence(value, ctx, it, end); } else if (*peek == '-') { // Block sequence if (preamble.tag != yaml::yaml_tag::none || preamble.node_props.has_anchor) { it = peek; } int32_t seq_indent = ctx.current_indent(); if (*it == '-' && ctx.current_indent() >= 0) { seq_indent = ctx.allow_indentless_sequence ? (ctx.current_indent() > 0 ? (ctx.current_indent() - 1) : 0) : (ctx.current_indent() + 1); } yaml::parse_block_sequence(value, ctx, it, end, seq_indent); } else { ctx.error = error_code::syntax_error; } yaml::finalize_node_anchor(preamble.node_props, ctx, it); } }; // Arrays template requires(readable_array_t && (emplace_backable || !resizable) && !emplaceable) struct from { template static void op(auto&& value, is_context auto&& ctx, It&& it, auto end) { if (bool(ctx.error)) [[unlikely]] return; // Peek ahead to check for tag (don't consume indentation for block sequences) auto peek = it; yaml::node_preamble_state preamble{}; if (yaml::parse_node_preamble(value, ctx, peek, end, preamble, [](yaml::yaml_tag tag) { return yaml::tag_valid_for_seq(tag); })) { it = peek; return; } if (*peek == '[') { // Flow sequence - consume whitespace and parse it = peek; yaml::parse_flow_sequence(value, ctx, it, end); } else if (*peek == '-') { // Block sequence - don't consume leading whitespace, let parse_block_sequence handle it // But if there was a tag, we need to advance past it if (preamble.tag != yaml::yaml_tag::none || preamble.node_props.has_anchor) { it = peek; } int32_t seq_indent = ctx.current_indent(); if (*it == '-' && ctx.current_indent() >= 0) { seq_indent = ctx.allow_indentless_sequence ? (ctx.current_indent() > 0 ? (ctx.current_indent() - 1) : 0) : (ctx.current_indent() + 1); } yaml::parse_block_sequence(value, ctx, it, end, seq_indent); } else { ctx.error = error_code::syntax_error; } yaml::finalize_node_anchor(preamble.node_props, ctx, it); } }; // Tuples (std::tuple, glaze_array_t, tuple_t) template requires(glaze_array_t || tuple_t || is_std_tuple) struct from { template static void op(auto&& value, is_context auto&& ctx, It&& it, auto end) { if (bool(ctx.error)) [[unlikely]] return; static constexpr auto N = []() constexpr { if constexpr (glaze_array_t) { return reflect::size; } else { return glz::tuple_size_v; } }(); yaml::skip_inline_ws(it, end); if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } // Check for tag const auto tag = yaml::parse_yaml_tag(it, end, ctx); if (tag == yaml::yaml_tag::unknown) [[unlikely]] { ctx.error = error_code::syntax_error; return; } if (!yaml::tag_valid_for_seq(tag)) [[unlikely]] { ctx.error = error_code::syntax_error; return; } yaml::skip_inline_ws(it, end); if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } if (*it == '[') { // Flow sequence ++it; // Skip '[' // Skip whitespace and newlines (YAML allows multi-line flow sequences) yaml::skip_ws_and_newlines(it, end); for_each([&]() { if (bool(ctx.error)) [[unlikely]] return; if (it != end && *it == ']') { if constexpr (check_error_on_missing_array_elements(Opts)) { ctx.error = error_code::array_element_not_found; } return; // Early termination } if constexpr (I != 0) { if (it == end || *it != ',') { ctx.error = error_code::syntax_error; return; } ++it; // Skip ',' yaml::skip_ws_and_newlines(it, end); } if constexpr (is_std_tuple) { using element_t = std::tuple_element_t>; from::template op()>(std::get(value), ctx, it, end); } else if constexpr (glaze_array_t) { using element_t = std::decay_t(meta_v)))>; from::template op()>( get_member(value, glz::get(meta_v)), ctx, it, end); } else { using element_t = std::decay_t(value))>; from::template op()>(glz::get(value), ctx, it, end); } yaml::skip_ws_and_newlines(it, end); }); if (bool(ctx.error)) [[unlikely]] return; if (it == end || *it != ']') { ctx.error = error_code::syntax_error; return; } ++it; // Skip ']' } else if (*it == '-') { // Block sequence size_t index = 0; while (it != end && index < N) { // Skip whitespace and measure indent auto line_start = it; yaml::skip_inline_ws(it, end); if (it == end) break; // Skip blank lines if (*it == '\n' || *it == '\r') { yaml::skip_newline(it, end); continue; } if (*it != '-') { it = line_start; break; } ++it; // Skip '-' // Check valid sequence indicator if (it != end && !yaml::whitespace_or_line_end_table[static_cast(*it)]) { it = line_start; break; } yaml::skip_inline_ws(it, end); // Parse element at current index [&](std::index_sequence) { ( [&]() { if (I == index) { if constexpr (is_std_tuple) { using element_t = std::tuple_element_t>; from::template op(std::get(value), ctx, it, end); } else if constexpr (glaze_array_t) { using element_t = std::decay_t(meta_v)))>; from::template op(get_member(value, glz::get(meta_v)), ctx, it, end); } else { using element_t = std::decay_t(value))>; from::template op(glz::get(value), ctx, it, end); } } }.template operator()(), ...); }(std::make_index_sequence{}); if (bool(ctx.error)) [[unlikely]] return; ++index; // Skip to next line yaml::skip_ws_and_comment(it, end); if (it != end && (*it == '\n' || *it == '\r')) { yaml::skip_newline(it, end); } } } else { ctx.error = error_code::syntax_error; } } }; // Pairs (std::pair) template struct from { template static void op(auto&& value, is_context auto&& ctx, It&& it, auto end) { if (bool(ctx.error)) [[unlikely]] return; yaml::skip_inline_ws(it, end); if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } // Check for tag - pairs are treated as single-entry mappings const auto tag = yaml::parse_yaml_tag(it, end, ctx); if (tag == yaml::yaml_tag::unknown) [[unlikely]] { ctx.error = error_code::syntax_error; return; } if (!yaml::tag_valid_for_map(tag)) [[unlikely]] { ctx.error = error_code::syntax_error; return; } yaml::skip_inline_ws(it, end); if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } using first_type = typename std::remove_cvref_t::first_type; using second_type = typename std::remove_cvref_t::second_type; if (*it == '{') { // Flow mapping style: {key: value} ++it; // Skip '{' yaml::skip_inline_ws(it, end); if (it != end && *it == '}') { ++it; return; // Empty pair } // Parse key if constexpr (str_t) { // Skip anchor on key if (*it == '&') { ++it; yaml::parse_anchor_name(it, end); yaml::skip_inline_ws(it, end); if (it == end) [[unlikely]] { ctx.error = error_code::unexpected_end; return; } } std::string key_str; const auto style = yaml::detect_scalar_style(*it); if (style == yaml::scalar_style::double_quoted) { yaml::parse_double_quoted_string(key_str, ctx, it, end); } else if (style == yaml::scalar_style::single_quoted) { yaml::parse_single_quoted_string(key_str, ctx, it, end); } else { yaml::parse_plain_scalar(key_str, ctx, it, end, true); } if (bool(ctx.error)) [[unlikely]] return; value.first = std::move(key_str); } else { from::template op()>(value.first, ctx, it, end); } if (bool(ctx.error)) [[unlikely]] return; yaml::skip_inline_ws(it, end); // Expect colon if (it == end || *it != ':') { ctx.error = error_code::syntax_error; return; } ++it; yaml::skip_inline_ws(it, end); // Parse value from::template op()>(value.second, ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; yaml::skip_inline_ws(it, end); if (it == end || *it != '}') { ctx.error = error_code::syntax_error; return; } ++it; // Skip '}' } else { // Block mapping style: key: value // Parse key if constexpr (str_t) { std::string key_str; if (!yaml::parse_yaml_key(key_str, ctx, it, end, false)) { return; } value.first = std::move(key_str); } else { from::template op(value.first, ctx, it, end); } if (bool(ctx.error)) [[unlikely]] return; yaml::skip_inline_ws(it, end); // Expect colon if (it == end || *it != ':') { ctx.error = error_code::syntax_error; return; } ++it; yaml::skip_inline_ws(it, end); // Parse value from::template op(value.second, ctx, it, end); } } }; // Objects (glaze_object_t and reflectable) template requires((glaze_object_t || reflectable) && !custom_read) struct from { // Tag is the discriminator key of an enclosing tagged variant (empty when not tagged). // When set, the mapping parsers skip an entry whose key equals the tag, since the // variant resolver has already consumed it to select this alternative. template static void op(auto&& value, is_context auto&& ctx, It&& it, auto end) { if (bool(ctx.error)) [[unlikely]] return; yaml::node_preamble_state preamble{}; if (yaml::parse_node_preamble( value, ctx, it, end, preamble, [](yaml::yaml_tag tag) { return yaml::tag_valid_for_map(tag); })) return; if (*it == '{') { // Flow mapping yaml::parse_flow_mapping(value, ctx, it, end); } else { // Block mapping - use indent from context (set by parent parser) yaml::parse_block_mapping(value, ctx, it, end, ctx.current_indent()); } yaml::finalize_node_anchor(preamble.node_props, ctx, it); } }; // Maps (std::map, std::unordered_map, etc.) template struct from { template static void op(auto&& value, is_context auto&& ctx, It&& it, auto end) { if (bool(ctx.error)) [[unlikely]] return; yaml::node_preamble_state preamble{}; if (yaml::parse_node_preamble( value, ctx, it, end, preamble, [](yaml::yaml_tag tag) { return yaml::tag_valid_for_map(tag); })) return; using key_t = typename std::remove_cvref_t::key_type; using val_t = typename std::remove_cvref_t::mapped_type; const bool treat_as_block_mapping = !yaml::check_flow_context(Opts) && *it == '{' && yaml::inline_value_has_plain_mapping_indicator(it, end); if (*it == '{' && !treat_as_block_mapping) { // Flow mapping ++it; // Skip whitespace and newlines after opening brace yaml::skip_ws_and_newlines(it, end); if (it != end && *it == '}') { ++it; yaml::validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; yaml::finalize_node_anchor(preamble.node_props, ctx, it); return; } while (it != end) { // Skip whitespace and newlines at start of each iteration yaml::skip_ws_and_newlines(it, end); if (it != end && *it == '}') { ++it; yaml::validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; break; } bool explicit_flow_key = false; auto explicit_probe = it; yaml::skip_inline_ws(explicit_probe, end); if (explicit_probe != end && *explicit_probe == '?') { const auto after_q = explicit_probe + 1; if (after_q == end || yaml::whitespace_or_line_end_table[static_cast(*after_q)] || *after_q == ',' || *after_q == '}') { explicit_flow_key = true; it = explicit_probe + 1; yaml::skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; } } // Parse key key_t key{}; if constexpr (std::same_as, std::string>) { if (explicit_flow_key && it != end && (*it == ':' || *it == ',' || *it == '}')) { key.clear(); } else { auto key_probe = it; yaml::skip_inline_ws(key_probe, end); // Use full node parsing only for structurally complex flow keys. // Plain/quoted/alias keys stay on parse_yaml_key() for compatibility. bool parse_complex_flow_key = false; if (key_probe != end) { if (*key_probe == '[' || *key_probe == '{') { parse_complex_flow_key = true; } else if (*key_probe == '&') { ++key_probe; yaml::parse_anchor_name(key_probe, end); yaml::skip_inline_ws(key_probe, end); if (key_probe != end && (*key_probe == '[' || *key_probe == '{')) { parse_complex_flow_key = true; } } } if (parse_complex_flow_key) { glz::generic key_node{}; from::template op()>(key_node, ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; if (key_node.is_null()) { key.clear(); } else if (auto* s = key_node.template get_if()) { key = *s; } else { std::string key_json; (void)glz::write_json(key_node, key_json); key = std::move(key_json); } } else { if (!yaml::parse_yaml_key(key, ctx, it, end, true)) { return; } } } } else { if (explicit_flow_key && it != end && (*it == ':' || *it == ',' || *it == '}')) { ctx.error = error_code::syntax_error; return; } from::template op()>(key, ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; } // Separation between flow key and ':' may include comments/newlines. // In flow context, newlines are allowed between key and ':'. yaml::skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; auto parse_implicit_null = [&](val_t& val) -> bool { if constexpr (nullable_like>) { val = {}; return true; } else if constexpr (std::same_as, glz::generic>) { val = glz::generic{}; return true; } else { static constexpr std::string_view null_value{"null"}; auto null_it = null_value.data(); from::template op()>( val, ctx, null_it, null_value.data() + null_value.size()); return !bool(ctx.error); } }; if (it != end && (*it == ',' || *it == '}')) { val_t val{}; if (!parse_implicit_null(val)) [[unlikely]] return; value.emplace(std::move(key), std::move(val)); if (*it == '}') { ++it; yaml::validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; break; } ++it; // skip comma yaml::skip_ws_and_newlines(it, end); continue; } if (it == end || *it != ':') { ctx.error = error_code::syntax_error; return; } ++it; yaml::skip_flow_ws_and_newlines(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; // Omitted value after an explicit ':' maps to an implicit null. if (it != end && (*it == ',' || *it == '}')) { val_t val{}; if (!parse_implicit_null(val)) [[unlikely]] return; value.emplace(std::move(key), std::move(val)); if (*it == '}') { ++it; yaml::validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; break; } ++it; // skip comma yaml::skip_ws_and_newlines(it, end); continue; } // Parse value val_t val{}; from::template op()>(val, ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; value.emplace(std::move(key), std::move(val)); yaml::skip_inline_ws(it, end); if (it != end && *it == '}') { ++it; yaml::validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; break; } else if (it != end && *it == ',') { ++it; // Skip whitespace and newlines after comma yaml::skip_ws_and_newlines(it, end); } else if (it != end && (*it == '\n' || *it == '\r')) { // Newlines may precede either the closing brace or the // separator comma for the next entry. yaml::skip_ws_and_newlines(it, end); if (it != end && *it == '}') { ++it; yaml::validate_flow_node_adjacent_tail(ctx, it, end); if (bool(ctx.error)) [[unlikely]] return; break; } if (it != end && *it == ',') { ++it; yaml::skip_ws_and_newlines(it, end); continue; } ctx.error = error_code::syntax_error; return; } else { ctx.error = error_code::syntax_error; return; } } } else { // Block mapping - use shared loop with map-specific callback yaml::parse_block_mapping_loop( ctx, it, end, int32_t(-1), [&](auto& ctx, auto& it, auto end, int32_t line_indent) -> bool { auto parse_map_value = [&](val_t& val) -> bool { if (it != end && !yaml::line_end_or_comment_table[static_cast(*it)]) { bool line_starts_with_explicit_value_indicator = false; // Inline mapping values in block context cannot start with a // plain "key: value" pair on the same line (e.g. "a: b: c"). // Such constructs are malformed unless wrapped in flow // delimiters ({...} / [...]) or quoted. if constexpr (std::is_pointer_v>) { if (ctx.stream_begin) { const auto begin = static_cast>(ctx.stream_begin); auto is_line_content_start = [&](auto pos) { auto line = pos; while (line != begin) { auto prev = line - 1; if (*prev == '\n' || *prev == '\r') break; line = prev; } for (auto p = line; p != pos; ++p) { if (*p != ' ' && *p != '\t') return false; } return true; }; auto line_has_explicit_value_indicator = [&](auto pos) { auto line = pos; while (line != begin) { auto prev = line - 1; if (*prev == '\n' || *prev == '\r') break; line = prev; } while (line != end && (*line == ' ' || *line == '\t')) ++line; return line != end && *line == ':'; }; if (!is_line_content_start(it) && yaml::inline_value_has_plain_mapping_indicator(it, end) && !line_has_explicit_value_indicator(it)) { ctx.error = error_code::syntax_error; return false; } line_starts_with_explicit_value_indicator = line_has_explicit_value_indicator(it); } } if (*it == '-' && ((it + 1) == end || yaml::whitespace_or_line_end_table[static_cast(*(it + 1))]) && !line_starts_with_explicit_value_indicator) { // "key: - item" is not valid block sequence syntax. ctx.error = error_code::syntax_error; return false; } if (!ctx.push_indent(line_indent + 1)) [[unlikely]] return false; from::template op(val, ctx, it, end); ctx.pop_indent(); } else { int32_t nested_indent = yaml::detect_nested_value_indent(ctx, it, end, line_indent); if (nested_indent >= 0) { yaml::skip_to_content(it, end); if (it != end && *it == ':' && (it + 1) != end && *(it + 1) == '\t') { ctx.error = error_code::syntax_error; return false; } if (!ctx.push_indent(nested_indent - 1)) [[unlikely]] return false; from::template op(val, ctx, it, end); ctx.pop_indent(); } } return !bool(ctx.error); }; // Explicit key entry form: // ? key // : value if (*it == '?' && ((it + 1) != end) && *(it + 1) == '\t') { ctx.error = error_code::syntax_error; return false; } if (*it == '?' && ((it + 1) == end || *(it + 1) == ' ' || *(it + 1) == '\n' || *(it + 1) == '\r')) { ++it; // skip '?' yaml::skip_inline_ws(it, end); const int32_t explicit_value_indicator_indent = (ctx.current_indent() < 0) ? 0 : (ctx.current_indent() + 1); key_t key{}; if constexpr (std::same_as, std::string>) { auto to_string_key = [&](glz::generic& key_node) { if (key_node.is_null()) { key.clear(); } else if (auto* s = key_node.template get_if()) { key = *s; } else { std::string key_json; (void)glz::write_json(key_node, key_json); key = std::move(key_json); } }; auto key_it = it; yaml::skip_inline_ws(key_it, end); auto content = key_it; yaml::skip_to_content(content, end); bool handled_anchor_only_empty_key = false; // Anchor on an empty explicit key node: // ? &a // : value if (content != end && *content == '&') { auto anchor_probe = content + 1; auto anchor_name = yaml::parse_anchor_name(anchor_probe, end); if (!anchor_name.empty()) { auto after_anchor = anchor_probe; yaml::skip_inline_ws(after_anchor, end); auto value_indicator = after_anchor; if (value_indicator != end && *value_indicator == ':') { key.clear(); ctx.anchors[std::string(anchor_name)] = {content, content, ctx.current_indent()}; it = value_indicator; handled_anchor_only_empty_key = true; } else { if (value_indicator != end && *value_indicator == '#') { yaml::skip_comment(value_indicator, end); } if (value_indicator != end && (*value_indicator == '\n' || *value_indicator == '\r')) { yaml::skip_newline(value_indicator, end); auto value_line = value_indicator; int32_t value_indent = yaml::measure_indent(value_line, end, ctx); if (bool(ctx.error)) [[unlikely]] return false; if (value_line != end && *value_line == ':' && value_indent == explicit_value_indicator_indent) { key.clear(); ctx.anchors[std::string(anchor_name)] = {content, content, ctx.current_indent()}; it = value_line; handled_anchor_only_empty_key = true; } else if (value_line != end && *value_line == ':') { // In explicit-key form ("? &a"), a following ':' // starts the outer value indicator only at the same // indentation. A deeper-indented ': value' is malformed. ctx.error = error_code::syntax_error; return false; } } } } } const bool key_content_spans_lines = [&] { auto scan = key_it; while (scan != content && scan != end) { if (*scan == '\n' || *scan == '\r') return true; ++scan; } return false; }(); if (handled_anchor_only_empty_key) { // 'it' already points to the explicit value indicator ':' } else if (content == end || (*content == ':' && key_content_spans_lines)) { key.clear(); it = content; } else { // Explicit mapping keys ('? key-node') are full YAML nodes. // Parse the key node structurally (not via plain-key scanning) // so inputs like '? []: x' and '? JSON: like' are interpreted // as complex keys, not split into implicit key/value pairs. auto key_node_it = content; auto key_node_end = end; if constexpr (std::is_pointer_v>) { // In explicit-key form, a following standalone ':' // line (same or lower indentation) belongs to the // outer mapping value, not the key node itself. auto scan = content; while (scan != end) { while (scan != end && *scan != '\n' && *scan != '\r') { ++scan; } if (scan == end) { break; } yaml::skip_newline(scan, end); auto line_start = scan; int32_t indicator_indent = 0; while (scan != end && *scan == ' ') { ++indicator_indent; ++scan; } if (scan != end && *scan == ':') { const auto after_colon = scan + 1; if ((after_colon == end || yaml::whitespace_or_line_end_table[static_cast(*after_colon)]) && indicator_indent <= line_indent) { key_node_end = line_start; break; } } } } glz::generic key_node{}; const bool prev_explicit_mapping_key_context = ctx.explicit_mapping_key_context; ctx.explicit_mapping_key_context = true; from::template op(key_node, ctx, key_node_it, key_node_end); ctx.explicit_mapping_key_context = prev_explicit_mapping_key_context; if (bool(ctx.error)) [[unlikely]] return false; it = key_node_it; to_string_key(key_node); } } else { if (it != end && !yaml::line_end_or_comment_table[static_cast(*it)]) { from::template op(key, ctx, it, end); if (bool(ctx.error)) [[unlikely]] return false; } } yaml::skip_inline_ws(it, end); yaml::skip_comment(it, end); // Value indicator may appear on the next line at the same indent. if (it != end && (*it == '\n' || *it == '\r')) { auto probe = it; bool found_value_indicator = false; while (probe != end && (*probe == '\n' || *probe == '\r')) { yaml::skip_newline(probe, end); auto value_line = probe; int32_t value_indent = yaml::measure_indent(value_line, end, ctx); if (bool(ctx.error)) [[unlikely]] return false; if (value_line == end || *value_line == '\n' || *value_line == '\r') { probe = value_line; continue; // blank line } if (*value_line == '#') { yaml::skip_comment(value_line, end); probe = value_line; continue; // comment-only line } if (*value_line == ':' && value_indent == explicit_value_indicator_indent) { it = value_line; found_value_indicator = true; } break; } if (found_value_indicator) { yaml::skip_inline_ws(it, end); } } val_t val{}; if (it != end && *it == ':') { ++it; if (it != end && *it == '\t') { ctx.error = error_code::syntax_error; return false; } if (it != end && (*it == '\n' || *it == '\r')) { auto probe = it; yaml::skip_newline(probe, end); while (probe != end && *probe == ' ') ++probe; if (probe != end && *probe == ':' && ((probe + 1) != end) && *(probe + 1) == '\t') { ctx.error = error_code::syntax_error; return false; } } yaml::skip_inline_ws(it, end); if (!parse_map_value(val)) [[unlikely]] return false; } value.emplace(std::move(key), std::move(val)); return true; } key_t key{}; if constexpr (std::same_as, std::string>) { auto key_probe = it; yaml::skip_inline_ws(key_probe, end); const bool complex_flow_key = (key_probe != end && (*key_probe == '[' || *key_probe == '{')); if (complex_flow_key) { glz::generic key_node{}; from::template op()>(key_node, ctx, it, end); if (bool(ctx.error)) [[unlikely]] return false; if (key_node.is_null()) { key.clear(); } else if (auto* s = key_node.template get_if()) { key = *s; } else { std::string key_json; (void)glz::write_json(key_node, key_json); key = std::move(key_json); } } else { if (!yaml::parse_yaml_key(key, ctx, it, end, false)) { return false; } } } else { from::template op(key, ctx, it, end); if (bool(ctx.error)) [[unlikely]] return false; } yaml::skip_inline_ws(it, end); if (it == end || *it != ':') { ctx.error = error_code::syntax_error; return false; } ++it; yaml::skip_inline_ws(it, end); val_t val{}; if (!parse_map_value(val)) [[unlikely]] return false; value.emplace(std::move(key), std::move(val)); return true; }); } yaml::finalize_node_anchor(preamble.node_props, ctx, it); } }; // ============================================================================ // YAML Variant type category traits // ============================================================================ template concept yaml_variant_bool_type = bool_t>; template concept yaml_variant_num_type = num_t>; template concept yaml_variant_str_type = str_t>; template concept yaml_variant_object_type = glaze_object_t || reflectable || writable_map_t || readable_map_t || is_memory_object; template concept yaml_variant_array_type = array_t> || glaze_array_t || tuple_t || is_std_tuple; template concept yaml_variant_null_type = null_t; // Type traits wrapping concepts for template template parameter use template struct is_yaml_variant_bool : std::bool_constant> {}; template struct is_yaml_variant_num : std::bool_constant> {}; template struct is_yaml_variant_str : std::bool_constant> {}; template struct is_yaml_variant_object : std::bool_constant> {}; template struct is_yaml_variant_array : std::bool_constant> {}; template struct is_yaml_variant_null : std::bool_constant> {}; // Count types in variant matching a trait template class Trait> struct yaml_variant_count_impl; template