class Gem::YAMLSerializer::Parser
Constants
- BLANK_OR_COMMENT_RE
- BLANK_RE
- FLOW_SEQUENCE_ENTRY_RE
-
An entry of a flow sequence. A quoted entry may contain commas.
- LSTRIP_BYTES
- MAPPING_KEY_RE
-
A plain (unquoted) mapping key followed by “:”. “#” inside a key is literal unless preceded by a space (which would start a comment).
- MAX_NESTING_DEPTH
- NULL_SCALARS
- QUOTED_KEY_RE
-
A quoted mapping key followed by “:”. A whole-line quoted scalar cannot match because its closing quote is at the end of the line.
- RUBY_OBJECT_TAG_RE
- SEQUENCE_ENTRY_RE
- STRING_UNESCAPES
Public Class Methods
Source
# File lib/rubygems/yaml_serializer.rb, line 59 def initialize(source) @lines = source.split(/\r?\n/).each(&:freeze) @anchors = {} @depth = 0 strip_document_prefix end
Public Instance Methods
Source
# File lib/rubygems/yaml_serializer.rb, line 66 def parse return nil if @lines.empty? root = nil while @lines.any? before = @lines.size node = parse_node(-1) @lines.shift if @lines.size == before && @lines.any? if root.is_a?(Mapping) && node.is_a?(Mapping) root.pairs.concat(node.pairs) elsif root.nil? root = node end end root end
Private Instance Methods
# File lib/rubygems/yaml_serializer.rb, line 252 def apply_tag(node, tag, anchor) if node.is_a?(Mapping) node.tag = tag node.anchor = anchor node else Mapping.new(["value", node], tag, anchor) end end
Source
# File lib/rubygems/yaml_serializer.rb, line 314 def coerce(val, depth = 0) raise_max_nesting! if depth > MAX_NESTING_DEPTH val = val.sub(/^! /, "") if val.start_with?("! ") if val.size >= 2 && val.start_with?("\"") && val.end_with?("\"") inner = val[1..-2] return inner unless inner.include?("\\") inner.gsub(/\\(?:["\\0abtnvfreN_LP]|x\h{2}|u\h{4}|U\h{8})/) do |m| char = STRING_UNESCAPES[m] || unescape_code_point(m) # Metadata is read as binary, where non-ASCII text stays as UTF-8 bytes. Encoding.compatible?(inner, char) ? char : char.b end elsif val.size >= 2 && val.start_with?("'") && val.end_with?("'") inner = val[1..-2] inner.include?("''") ? inner.gsub("''", "'") : inner elsif val == "true" true elsif val == "false" false elsif NULL_SCALARS.include?(val) nil elsif val == "{}" Mapping.new([]) elsif val =~ /^\[(.*)\]$/ inner = $1.strip return Sequence.new([]) if inner.empty? items = inner.scan(FLOW_SEQUENCE_ENTRY_RE).map {|e| Scalar.new(coerce(e, depth + 1)) } Sequence.new(items) elsif /\A\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2})?/.match?(val) begin Time.new(val) rescue ArgumentError # date-only format like "2024-06-15" is not supported by Time.new if /\A(\d{4})-(\d{2})-(\d{2})\z/.match(val) Time.utc($1.to_i, $2.to_i, $3.to_i) else val end end elsif /^-?\d+$/.match?(val) val.to_i else val end end
Source
# File lib/rubygems/yaml_serializer.rb, line 401 def consume_anchor line = @lines[0] stripped = line.lstrip return nil unless stripped.start_with?("&") && stripped =~ /^&(\S+)\s+/ anchor = $1 @lines[0] = line.sub(/&#{Regexp.escape(anchor)}\s+/, "") anchor end
Source
# File lib/rubygems/yaml_serializer.rb, line 418 def consume_value_anchor(val) return [nil, val] unless val =~ /^&(\S+)\s+/ anchor = $1 [anchor, val.sub(/^&#{Regexp.escape(anchor)}\s+/, "")] end
Source
# File lib/rubygems/yaml_serializer.rb, line 391 def current_indent indent_of(@lines[0]) end
Source
# File lib/rubygems/yaml_serializer.rb, line 367 def decode_binary_tag(str) content = str.sub(/\A!binary\s+/, "") content = $1 if content =~ /\A"(.*)"\z/ || content =~ /\A'(.*)'\z/ content.unpack1("m") end
# File lib/rubygems/yaml_serializer.rb, line 411 def extract_item_anchor(content) return [nil, content] unless content =~ /^&(\S+)/ anchor = $1 [anchor, content.sub(/^&#{Regexp.escape(anchor)}\s*/, "")] end
# File lib/rubygems/yaml_serializer.rb, line 304 def fold_continuation_lines(result, val, indent) return unless result.is_a?(String) # "#" does not start a comment inside a quoted scalar that runs onto the next lines. stop = val.start_with?("'", "\"") && result == val ? BLANK_RE : BLANK_OR_COMMENT_RE while @lines.any? && !stop.match?(@lines[0]) && current_indent > indent result << " " << @lines.shift.strip end end
Source
# File lib/rubygems/yaml_serializer.rb, line 395 def indent_of(line) i = 0 i += 1 while LSTRIP_BYTES.include?(line.getbyte(i)) i end
Source
# File lib/rubygems/yaml_serializer.rb, line 383 def parse_alias_ref AliasRef.new(@lines.shift.lstrip[1..].strip) end
# File lib/rubygems/yaml_serializer.rb, line 373 def parse_binary_value(val, indent) rest = val.sub(/\A!binary\s+/, "") if rest.start_with?("|") content = parse_block_scalar(indent, rest[1..].to_s.strip) Scalar.new(content.unpack1("m")) else Scalar.new(decode_binary_tag(val)) end end
# File lib/rubygems/yaml_serializer.rb, line 262 def parse_block_scalar(base_indent, modifier) parts = [] indentation = modifier[/\A[+-]?([1-9])/, 1] chomping = modifier[/\A[1-9]?([+-])/, 1] block_indent = indentation && base_indent + indentation.to_i while @lines.any? line = @lines[0] if BLANK_RE.match?(line) parts << line[block_indent..] if block_indent && line.size > block_indent parts << "\n" @lines.shift else line_indent = indent_of(line) break if line_indent <= base_indent block_indent ||= line_indent parts << @lines.shift[block_indent..].to_s << "\n" end end res = parts.join res.delete_suffix!("\n") while chomping == "-" && res.end_with?("\n") res end
Source
# File lib/rubygems/yaml_serializer.rb, line 387 def parse_inline_alias(content) AliasRef.new(content[1..].strip) end
# File lib/rubygems/yaml_serializer.rb, line 296 def parse_inline_scalar(val, indent) result = coerce(val) return result if result.is_a?(Mapping) || result.is_a?(Sequence) fold_continuation_lines(result, val, indent) Scalar.new(result) end
# File lib/rubygems/yaml_serializer.rb, line 183 def parse_mapping(indent, anchor) pairs = [] while @lines.any? skip_blank_and_comments break if @lines.empty? line = @lines[0] break unless indent_of(line) == indent if (match = QUOTED_KEY_RE.match(line, indent)) key = coerce(match[1]) elsif (match = MAPPING_KEY_RE.match(line, indent)) && !RUBY_OBJECT_TAG_RE.match?(line, indent) key = match[1] key.strip! key = decode_binary_tag(key) if key.start_with?("!binary ") else break end @lines.shift val = match[2] val&.strip! val = strip_comment(val || "") val_anchor = nil val_anchor, val = consume_value_anchor(val) if val.start_with?("&") value = parse_mapping_value(val, indent) value = register_anchor(val_anchor, value) if val_anchor pairs << key << value end register_anchor(anchor, Mapping.new(pairs)) end
# File lib/rubygems/yaml_serializer.rb, line 215 def parse_mapping_value(val, indent) if val.start_with?("*") parse_inline_alias(val) elsif val.start_with?("!ruby/object:") parse_tagged_content(val.strip, indent) elsif val.start_with?("!binary ") parse_binary_value(val, indent) elsif val.empty? skip_blank_and_comments line = @lines[0] if line && indent_of(line) == indent && SEQUENCE_ENTRY_RE.match?(line, indent) parse_node(indent) else parse_node(indent + 1) end elsif val == "[]" Sequence.new([]) elsif val == "{}" Mapping.new([]) elsif val.start_with?("|") Scalar.new(parse_block_scalar(indent, val[1..].to_s.strip)) else parse_inline_scalar(val, indent) end end
Source
# File lib/rubygems/yaml_serializer.rb, line 97 def parse_node(base_indent) @depth += 1 raise_max_nesting! if @depth > MAX_NESTING_DEPTH skip_blank_and_comments return nil if @lines.empty? line = @lines[0] indent = indent_of(line) return nil if indent < base_indent return parse_alias_ref if /\G\*/.match?(line, indent) pos = indent if /\G&/.match?(line, indent) && (anchor = consume_anchor) line = @lines[0] pos = indent_of(line) end if SEQUENCE_ENTRY_RE.match?(line, pos) parse_sequence(indent, anchor) elsif QUOTED_KEY_RE.match?(line, pos) # A mapping whose key is quoted, e.g. '"have: colon": value'. parse_mapping(indent, anchor) elsif (/\G"/.match?(line, pos) && line.end_with?("\"")) || (/\G'/.match?(line, pos) && line.end_with?("'")) # A whole-line quoted scalar, e.g. '"system: foo: bar"'. It may # contain ": ", so it must be checked before MAPPING_KEY_RE. parse_plain_scalar(indent, anchor) elsif MAPPING_KEY_RE.match?(line, pos) && !RUBY_OBJECT_TAG_RE.match?(line, pos) parse_mapping(indent, anchor) elsif RUBY_OBJECT_TAG_RE.match?(line, pos) parse_tagged_node(indent, anchor) elsif /\G\|/.match?(line, pos) modifier = line[pos + 1, line.size].strip @lines.shift register_anchor(anchor, Scalar.new(parse_block_scalar(indent, modifier))) else parse_plain_scalar(indent, anchor) end ensure @depth -= 1 end
# File lib/rubygems/yaml_serializer.rb, line 287 def parse_plain_scalar(indent, anchor) val = @lines.shift.strip result = coerce(val) return register_anchor(anchor, result) if result.is_a?(Mapping) || result.is_a?(Sequence) fold_continuation_lines(result, val, indent) register_anchor(anchor, Scalar.new(result)) end
# File lib/rubygems/yaml_serializer.rb, line 141 def parse_sequence(indent, anchor) items = [] while @lines.any? skip_blank_and_comments break if @lines.empty? line = @lines[0] break unless indent_of(line) == indent && SEQUENCE_ENTRY_RE.match?(line, indent) @lines.shift content = line[indent + 1, line.size] content.strip! item_anchor = nil item_anchor, content = extract_item_anchor(content) if content.start_with?("&") item = parse_sequence_item(content, indent) items << register_anchor(item_anchor, item) end register_anchor(anchor, Sequence.new(items)) end
# File lib/rubygems/yaml_serializer.rb, line 159 def parse_sequence_item(content, indent) if content.start_with?("*") parse_inline_alias(content) elsif content.empty? skip_blank_and_comments @lines.any? && current_indent > indent ? parse_node(indent) : nil elsif content.start_with?("!ruby/object:") parse_tagged_content(content.strip, indent) elsif content.start_with?("!binary ") parse_binary_value(content, indent) elsif content.start_with?("-") @lines.unshift("#{" " * (indent + 2)}#{content}") parse_node(indent) elsif QUOTED_KEY_RE.match?(content) || (MAPPING_KEY_RE.match?(content) && !content.start_with?("!ruby/object:")) @lines.unshift("#{" " * (indent + 2)}#{content}") parse_node(indent) elsif content.start_with?("|") Scalar.new(parse_block_scalar(indent, content[1..].to_s.strip)) else parse_inline_scalar(content, indent) end end
# File lib/rubygems/yaml_serializer.rb, line 247 def parse_tagged_content(tag, indent) nested = parse_node(indent) apply_tag(nested, tag, nil) end
# File lib/rubygems/yaml_serializer.rb, line 241 def parse_tagged_node(indent, anchor) tag = @lines.shift.strip nested = parse_node(indent) apply_tag(nested, tag, anchor) end
Source
# File lib/rubygems/yaml_serializer.rb, line 433 def raise_max_nesting! message = "exceeded maximum nesting depth (#{MAX_NESTING_DEPTH})" if defined?(Psych::VERSION) raise Psych::SyntaxError.new(nil, 0, 0, 0, message, nil) else raise Psych::SyntaxError, message end end
Source
# File lib/rubygems/yaml_serializer.rb, line 425 def register_anchor(name, node) if name @anchors[name] = node node.anchor = name if node.respond_to?(:anchor=) end node end
Source
# File lib/rubygems/yaml_serializer.rb, line 442 def skip_blank_and_comments @lines.shift while @lines.any? && BLANK_OR_COMMENT_RE.match?(@lines[0]) end
Source
# File lib/rubygems/yaml_serializer.rb, line 446 def strip_comment(val) return val unless val.include?("#") return "" if val.lstrip.start_with?("#") in_single = false in_double = false escape = false val.each_char.with_index do |ch, i| if escape escape = false next end if in_single in_single = false if ch == "'" elsif in_double if ch == "\\" escape = true elsif ch == '"' in_double = false end else case ch when "'" then in_single = true when '"' then in_double = true when "#" # A "#" starts a comment only when preceded by whitespace. return val[0...i].rstrip if [" ", "\t"].include?(val[i - 1]) end end end val end
Source
# File lib/rubygems/yaml_serializer.rb, line 86 def strip_document_prefix return if @lines.empty? return unless @lines[0]&.start_with?("---") if @lines[0].strip == "---" @lines.shift else @lines[0] = @lines[0].sub(/^---\s*/, "") end end
Source
# File lib/rubygems/yaml_serializer.rb, line 361 def unescape_code_point(escape) code = escape[2..].hex return escape if code > 0x10FFFF || code.between?(0xD800, 0xDFFF) code.chr(Encoding::UTF_8) end