From 55f4dc4c9a5345c28d0da750d1ee00fbb0870885 Mon Sep 17 00:00:00 2001 From: why Date: Fri, 9 May 2003 21:25:50 +0000 Subject: Initial checkin of YAML substances. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@3772 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/yaml.rb | 119 +++++++++++ lib/yaml/basenode.rb | 216 +++++++++++++++++++ lib/yaml/constants.rb | 51 +++++ lib/yaml/dbm.rb | 111 ++++++++++ lib/yaml/emitter.rb | 330 +++++++++++++++++++++++++++++ lib/yaml/encoding.rb | 123 +++++++++++ lib/yaml/error.rb | 33 +++ lib/yaml/rubytypes.rb | 558 ++++++++++++++++++++++++++++++++++++++++++++++++++ lib/yaml/store.rb | 75 +++++++ lib/yaml/stream.rb | 44 ++++ lib/yaml/stringio.rb | 83 ++++++++ lib/yaml/syck.rb | 19 ++ lib/yaml/types.rb | 195 ++++++++++++++++++ lib/yaml/yamlnode.rb | 54 +++++ lib/yaml/ypath.rb | 52 +++++ 15 files changed, 2063 insertions(+) create mode 100644 lib/yaml.rb create mode 100644 lib/yaml/basenode.rb create mode 100644 lib/yaml/constants.rb create mode 100644 lib/yaml/dbm.rb create mode 100644 lib/yaml/emitter.rb create mode 100644 lib/yaml/encoding.rb create mode 100644 lib/yaml/error.rb create mode 100644 lib/yaml/rubytypes.rb create mode 100644 lib/yaml/store.rb create mode 100644 lib/yaml/stream.rb create mode 100644 lib/yaml/stringio.rb create mode 100644 lib/yaml/syck.rb create mode 100644 lib/yaml/types.rb create mode 100644 lib/yaml/yamlnode.rb create mode 100644 lib/yaml/ypath.rb (limited to 'lib') diff --git a/lib/yaml.rb b/lib/yaml.rb new file mode 100644 index 0000000000..3b669d3723 --- /dev/null +++ b/lib/yaml.rb @@ -0,0 +1,119 @@ +# vim:sw=4:ts=4 +# $Id$ +# +# YAML.rb +# +# Loads the parser/loader and emitter/writer. +# + +module YAML + + begin + require 'syck' + Parser = YAML::Syck::Parser + rescue LoadError + require 'yaml/parser' + Parser = YAML::Parser + end + require 'yaml/emitter' + require 'yaml/rubytypes' + + # + # Allocate blank object + # + def YAML.object_maker( obj_class, val ) + if Hash === val + name = obj_class.name + o = ::Marshal.load( sprintf( "\004\006o:%c%s\000", name.length + 5, name )) + val.each_pair { |k,v| + o.instance_eval "@#{k} = v" + } + o + else + raise YAML::Error, "Invalid object explicitly tagged !ruby/Object: " + val.inspect + end + end + + # + # Input methods + # + + # + # Load a single document from the current stream + # + def YAML.load( io ) + yp = YAML::Parser.new.parse( io ) + end + + # + # Parse a single document from the current stream + # + def YAML.parse( io ) + yp = YAML::Parser.new( :Model => :Generic ).parse( io ) + end + + # + # Load all documents from the current stream + # + def YAML.each_document( io, &doc_proc ) + yp = YAML::Parser.new.parse_documents( io, &doc_proc ) + end + + # + # Identical to each_document + # + def YAML.load_documents( io, &doc_proc ) + YAML.each_document( io, &doc_proc ) + end + + # + # Parse all documents from the current stream + # + def YAML.each_node( io, &doc_proc ) + yp = YAML::Parser.new( :Model => :Generic ).parse_documents( io, &doc_proc ) + end + + # + # Parse all documents from the current stream + # + def YAML.parse_documents( io, &doc_proc ) + YAML.each_node( io, &doc_proc ) + end + + # + # Load all documents from the current stream + # + def YAML.load_stream( io ) + yp = YAML::Parser.new + d = nil + yp.parse_documents( io ) { |doc| + d = YAML::Stream.new( yp.options ) if not d + d.add( doc ) + } + return d + end + +end + +# +# ryan: You know how Kernel.p is a really convenient way to dump ruby +# structures? The only downside is that it's not as legible as +# YAML. +# +# _why: (listening) +# +# ryan: I know you don't want to urinate all over your users' namespaces. +# But, on the other hand, convenience of dumping for debugging is, +# IMO, a big YAML use case. +# +# _why: Go nuts! Have a pony parade! +# +# ryan: Either way, I certainly will have a pony parade. +# +module Kernel + def y( x ) + puts x.to_yaml + end +end + + diff --git a/lib/yaml/basenode.rb b/lib/yaml/basenode.rb new file mode 100644 index 0000000000..341177a979 --- /dev/null +++ b/lib/yaml/basenode.rb @@ -0,0 +1,216 @@ +# +# YAML::BaseNode class +# +require 'yaml/ypath' + +module YAML + + # + # YAML Generic Model container + # + module BaseNode + + # + # Search for YPath entry and return + # qualified nodes. + # + def select( ypath_str ) + matches = match_path( ypath_str ) + + # + # Create a new generic view of the elements selected + # + if matches + result = [] + matches.each { |m| + result.push m.last + } + YamlNode.new( 'seq', result ) + end + end + + # + # Search for YPath entry and return + # transformed nodes. + # + def select!( ypath_str ) + matches = match_path( ypath_str ) + + # + # Create a new generic view of the elements selected + # + if matches + result = [] + matches.each { |m| + result.push m.last.transform + } + result + end + end + + # + # Search for YPath entry and return a list of + # qualified paths. + # + def search( ypath_str ) + matches = match_path( ypath_str ) + + if matches + matches.collect { |m| + path = [] + m.each_index { |i| + path.push m[i] if ( i % 2 ).zero? + } + "/" + path.compact.join( "/" ) + } + end + end + + def at( seg ) + if Hash === @value and @value.has_key?( seg ) + @value[seg][1] + elsif Array === @value and seg =~ /\A\d+\Z/ and @value[seg.to_i] + @value[seg.to_i] + end + end + + # + # YPath search returning a complete depth array + # + def match_path( ypath_str ) + depth = 0 + matches = [] + YPath.each_path( ypath_str ) do |ypath| + seg = match_segment( ypath, 0 ) + matches += seg if seg + end + matches.uniq + end + + # + # Search a node for a single YPath segment + # + def match_segment( ypath, depth ) + deep_nodes = [] + seg = ypath.segments[ depth ] + if seg == "/" + unless String === @value + idx = -1 + @value.collect { |v| + idx += 1 + if Hash === @value + match_init = [v[0], v[1][1]] + match_deep = v[1][1].match_segment( ypath, depth ) + else + match_init = [idx, v] + match_deep = v.match_segment( ypath, depth ) + end + if match_deep + match_deep.each { |m| + deep_nodes.push( match_init + m ) + } + end + } + end + depth += 1 + seg = ypath.segments[ depth ] + end + match_nodes = + case seg + when "." + [[nil, self]] + when ".." + [["..", nil]] + when "*" + if @value.is_a? Enumerable + idx = -1 + @value.collect { |h| + idx += 1 + if Hash === @value + [h[0], h[1][1]] + else + [idx, h] + end + } + end + else + if seg =~ /^"(.*)"$/ + seg = $1 + elsif seg =~ /^'(.*)'$/ + seg = $1 + end + if ( v = at( seg ) ) + [[ seg, v ]] + end + end + return deep_nodes unless match_nodes + pred = ypath.predicates[ depth ] + if pred + case pred + when /^\.=/ + pred = $' + match_nodes.reject! { |n| + n.last.value != pred + } + else + match_nodes.reject! { |n| + n.last.at( pred ).nil? + } + end + end + return match_nodes + deep_nodes unless ypath.segments.length > depth + 1 + + #puts "DEPTH: #{depth + 1}" + deep_nodes = [] + match_nodes.each { |n| + if n[1].is_a? YamlNode + match_deep = n[1].match_segment( ypath, depth + 1 ) + if match_deep + match_deep.each { |m| + deep_nodes.push( n + m ) + } + end + else + deep_nodes = [] + end + } + deep_nodes = nil if deep_nodes.length == 0 + deep_nodes + end + + # + # We want the node to act like as Hash + # if it is. + # + def []( *k ) + if Hash === @value + v = @value.[]( *k ) + v[1] if v + elsif Array === @value + @value.[]( *k ) + end + end + + def children + if Hash === @value + @value.values.collect { |c| c[1] } + elsif Array === @value + @value + end + end + + def children_with_index + if Hash === @value + @value.keys.collect { |i| [self[i], i] } + elsif Array === @value + i = -1; @value.collect { |v| i += 1; [v, i] } + end + end + + def emit + transform.to_yaml + end + end + +end + diff --git a/lib/yaml/constants.rb b/lib/yaml/constants.rb new file mode 100644 index 0000000000..21aee7e0a6 --- /dev/null +++ b/lib/yaml/constants.rb @@ -0,0 +1,51 @@ +# +# Constants used throughout the library +# +module YAML + + # + # Constants + # + VERSION = '0.60' + SUPPORTED_YAML_VERSIONS = ['1.0'] + + # + # Parser tokens + # + WORD_CHAR = 'A-Za-z0-9' + PRINTABLE_CHAR = '-_A-Za-z0-9!?/()$\'". ' + NOT_PLAIN_CHAR = '\x7f\x0-\x1f\x80-\x9f' + ESCAPE_CHAR = '[\\x00-\\x08\\x0b-\\x0d\\x0e-\\x1f]' + INDICATOR_CHAR = '*&!|\\\\^@%{}[]=' + SPACE_INDICATORS = '-#:,?' + RESTRICTED_INDICATORS = '#:,}]' + DNS_COMP_RE = "\\w(?:[-\\w]*\\w)?" + DNS_NAME_RE = "(?:(?:#{DNS_COMP_RE}\\.)+#{DNS_COMP_RE}|#{DNS_COMP_RE})" + ESCAPES = %w{\z \x01 \x02 \x03 \x04 \x05 \x06 \a + \x08 \t \n \v \f \r \x0e \x0f + \x10 \x11 \x12 \x13 \x14 \x15 \x16 \x17 + \x18 \x19 \x1a \e \x1c \x1d \x1e \x1f + } + UNESCAPES = { + 'z' => "\x00", 'a' => "\x07", 'b' => "\x08", 't' => "\x09", + 'n' => "\x0a", 'v' => "\x0b", 'f' => "\x0c", + 'r' => "\x0d", 'e' => "\x1b", '\\' => '\\', + } + + # + # Default settings + # + DEFAULTS = { + :Indent => 2, :UseHeader => false, :UseVersion => false, :Version => '1.0', + :SortKeys => false, :AnchorFormat => 'id%03d', :ExplicitTypes => false, + :WidthType => 'absolute', :BestWidth => 80, + :UseBlock => false, :UseFold => false, :Encoding => :None + } + TRANSFER_DOMAINS = { + 'yaml.org,2002' => {}, + 'ruby.yaml.org,2002' => {} + } + PRIVATE_TYPES = {} + IMPLICIT_TYPES = [ 'null', 'bool', 'time', 'int', 'float' ] + +end diff --git a/lib/yaml/dbm.rb b/lib/yaml/dbm.rb new file mode 100644 index 0000000000..b1474a736a --- /dev/null +++ b/lib/yaml/dbm.rb @@ -0,0 +1,111 @@ +require 'yaml' +require 'dbm' +# +# YAML + DBM = YDBM +# - Same interface as DBM class +# +module YAML + +class DBM < ::DBM + VERSION = "0.1" + def []( key ) + fetch( key ) + end + def []=( key, val ) + store( key, val ) + end + def fetch( keystr, ifnone = nil ) + begin + val = super( keystr ) + return YAML::load( val ) if String === val + rescue IndexError + end + if block_given? + yield keystr + else + ifnone + end + end + def index( keystr ) + super( keystr.to_yaml ) + end + def indexes( *keys ) + keys.collect { |k| fetch( k ) } + end + def delete( key ) + v = super( key ) + if String === v + v = YAML::load( v ) + end + v + end + def delete_if + del_keys = keys.dup + del_keys.delete_if { |k| yield( k, fetch( k ) ) == false } + del_keys.each { |k| delete( k ) } + self + end + def reject + hsh = self.to_hash + hsh.reject { |k,v| yield k, v } + end + def each_pair + keys.each { |k| yield k, fetch( k ) } + self + end + def each_value + super { |v| yield YAML::load( v ) } + self + end + def values + super.collect { |v| YAML::load( v ) } + end + def has_value?( val ) + each_value { |v| return true if v == val } + return false + end + def invert + h = {} + keys.each { |k| h[ self.fetch( k ) ] = k } + h + end + def replace( hsh ) + clear + update( hsh ) + end + def shift + a = super + a[1] = YAML::load( a[1] ) if a + a + end + def select( *keys ) + if block_given? + self.keys.collect { |k| v = self[k]; [k, v] if yield k, v }.compact + else + indexes( *keys ) + end + end + def store( key, val ) + super( key, val.to_yaml ) + val + end + def update( hsh ) + hsh.keys.each do |k| + self.store( k, hsh.fetch( k ) ) + end + self + end + def to_a + a = [] + keys.each { |k| a.push [ k, self.fetch( k ) ] } + a + end + def to_hash + h = {} + keys.each { |k| h[ k ] = self.fetch( k ) } + h + end + alias :each :each_pair +end + +end diff --git a/lib/yaml/emitter.rb b/lib/yaml/emitter.rb new file mode 100644 index 0000000000..4b8541c2c3 --- /dev/null +++ b/lib/yaml/emitter.rb @@ -0,0 +1,330 @@ +# +# Output classes and methods +# + +require 'yaml/constants' +require 'yaml/encoding' +require 'yaml/error' + +module YAML + + # + # Emit a set of values + # + + class Emitter + attr_accessor :options + def initialize( opts ) + opts = {} if opts.class != Hash + @options = YAML::DEFAULTS.dup.update( opts ) + @headless = 0 + @seq_map = false + @anchors = {} + @anchor_extras = {} + @active_anchors = [] + @level = -1 + self.clear + end + + def clear + @buffer = [] + end + + # + # Version string + # + def version_s + " %YAML:#{@options[:Version]}" if @options[:UseVersion] + end + + # + # Header + # + def header + if @headless.nonzero? + "" + else + "---#{version_s} " + end + end + + # + # Emit binary data + # + def binary_base64( value ) + self << "!binary " + self.node_text( [value].pack("m"), '|' ) + end + + # + # Emit plain, normal flowing text + # + def node_text( value, block = '>' ) + valx = value.dup + if @options[:UseBlock] + block = '|' + elsif not @options[:UseFold] and valx =~ /\n[ \t]/ and not valx =~ /#{YAML::ESCAPE_CHAR}/ + block = '|' + end + str = block.dup + if valx =~ /\n\Z\n/ + str << "+" + elsif valx =~ /\Z\n/ + else + str << "-" + end + if valx =~ /#{YAML::ESCAPE_CHAR}/ + valx = YAML::escape( valx ) + end + if valx =~ /\A[ \t#]/ + str << @options[:Indent].to_s + end + if block == '>' + valx = fold( valx ) + end + self << str + indent_text( valx ) + "\n" + end + + # + # Emit a simple, unqouted string + # + def simple( value ) + self << value.to_s + end + + # + # Emit double-quoted string + # + def double( value ) + "\"#{YAML.escape( value )}\"" + end + + # + # Emit single-quoted string + # + def single( value ) + "'#{value}'" + end + + # + # Write a text block with the current indent + # + def indent_text( text ) + return "" if text.to_s.empty? + spacing = " " * ( @level * @options[:Indent] ) + return "\n" + text.gsub( /^([^\n])/, "#{spacing}\\1" ) + end + + # + # Write a current indent + # + def indent + #p [ self.id, @level, :INDENT ] + return " " * ( @level * @options[:Indent] ) + end + + # + # Add indent to the buffer + # + def indent! + self << indent + end + + # + # Folding paragraphs within a column + # + def fold( value ) + value.gsub!( /\A\n+/, '' ) + folded = $&.to_s + width = (0..@options[:BestWidth]) + while not value.empty? + last = value.index( /(\n+)/ ) + chop_s = false + if width.include?( last ) + last += $1.length - 1 + elsif width.include?( value.length ) + last = value.length + else + last = value.rindex( /[ \t]/, @options[:BestWidth] ) + chop_s = true + end + folded += value.slice!( 0, width.include?( last ) ? last + 1 : @options[:BestWidth] ) + folded.chop! if chop_s + folded += "\n" unless value.empty? + end + folded + end + + # + # Quick mapping + # + def map( type, &e ) + val = Mapping.new + e.call( val ) + self << "#{type} " if type.length.nonzero? + + # + # Empty hashes + # + if val.length.zero? + self << "{}" + else + if @buffer.length == 1 and @options[:UseHeader] == false and type.length.zero? + @headless = 1 + end + + defkey = @options.delete( :DefaultKey ) + if defkey + seq_map_shortcut + self << "= : " + defkey.to_yaml( :Emitter => self ) + end + + # + # Emit the key and value + # + val.each { |v| + seq_map_shortcut + if v[0].is_complex_yaml? + self << "? " + end + v[0].to_yaml( :Emitter => self ) + if v[0].is_complex_yaml? + self << "\n" + indent! + end + self << ": " + v[1].to_yaml( :Emitter => self ) + } + end + end + + def seq_map_shortcut + if @seq_map + @anchor_extras[@buffer.length - 1] = "\n" + indent + @seq_map = false + else + self << "\n" + indent! + end + end + + # + # Quick sequence + # + def seq( type, &e ) + val = Sequence.new + e.call( val ) + self << "#{type} " if type.length.nonzero? + + # + # Empty arrays + # + if val.length.zero? + self << "[]" + else + if @buffer.length == 1 and @options[:UseHeader] == false and type.length.zero? + @headless = 1 + end + # + # Emit the key and value + # + val.each { |v| + self << "\n" + indent! + self << "- " + @seq_map = true if v.class == Hash + v.to_yaml( :Emitter => self ) + } + end + end + + # + # Concatenate to the buffer + # + def <<( str ) + #p [ self.id, @level, str ] + @buffer.last << str + end + + # + # Monitor objects and allow references + # + def start_object( oid ) + @level += 1 + @buffer.push( "" ) + #p [ self.id, @level, :OPEN ] + idx = nil + if oid + if @anchors.has_key?( oid ) + idx = @active_anchors.index( oid ) + unless idx + idx = @active_anchors.length + af_str = "&#{@options[:AnchorFormat]} " % [ idx + 1 ] + af_str += @anchor_extras[ @anchors[ oid ] ].to_s + @buffer[ @anchors[ oid ] ][0,0] = af_str + @headless = 0 if @anchors[ oid ].zero? + end + idx += 1 + @active_anchors.push( oid ) + else + @anchors[ oid ] = @buffer.length - 1 + end + end + return idx + end + + # + # Output method + # + def end_object + @level -= 1 + @buffer.push( "" ) + #p [ self.id, @level, :END ] + if @level < 0 + YAML.internal_to_utf( header + @buffer.to_s[@headless..-1], @options[:Encoding] ) + end + end + end + + # + # Emitter helper classes + # + class Mapping < Array + def add( k, v ) + push [k, v] + end + end + + class Sequence < Array + def add( v ) + push v + end + end + + # + # Allocate an Emitter if needed + # + def YAML.quick_emit( oid, opts = {}, &e ) + old_opt = nil + if opts[:Emitter].is_a? YAML::Emitter + out = opts.delete( :Emitter ) + old_opt = out.options.dup + out.options.update( opts ) + else + out = YAML::Emitter.new( opts ) + end + aidx = out.start_object( oid ) + if aidx + out.simple( "*#{out.options[:AnchorFormat]} " % [ aidx ] ) + else + e.call( out ) + end + if old_opt.is_a? Hash + out.options = old_opt + end + out.end_object + end + +end + diff --git a/lib/yaml/encoding.rb b/lib/yaml/encoding.rb new file mode 100644 index 0000000000..59491b53b7 --- /dev/null +++ b/lib/yaml/encoding.rb @@ -0,0 +1,123 @@ +# +# Handle Unicode-to-Internal conversion +# + +module YAML + + # + # Encodings ( $-K to ICONV ) + # + CHARSETS = { + 'NONE' => 'LATIN1', + 'ASCII' => 'US-ASCII', + 'UTF-8' => 'UTF-8', + 'EUC' => 'EUC-JP', + 'SJIS' => 'SHIFT-JIS' + } + + # + # YAML documents can be in UTF-8, UTF-16 or UTF-32 + # So let's read and write in Unicode + # + + @@unicode = false + begin + require 'iconv' + DEFAULTS[:Encoding] = :Utf8 + rescue LoadError + end + + def YAML.unicode; @@unicode; end + def YAML.unicode=( bool ); @@unicode = bool; end + + # + # Unicode conversion + # + + def YAML.utf_to_internal( str, from_enc ) + return unless str + to_enc = CHARSETS[$-K] + case from_enc + when :Utf32 + Iconv.iconv( to_enc, 'UTF-32', str )[0] + when :Utf16 + Iconv.iconv( to_enc, 'UTF-16', str )[0] + when :Utf8 + Iconv.iconv( to_enc, 'UTF-8', str )[0] + when :None + str + else + raise YAML::Error, ERROR_UNSUPPORTED_ENCODING % from_enc.inspect + end + end + + def YAML.internal_to_utf( str, to_enc ) + return unless str + from_enc = CHARSETS[$-K] + case to_enc + when :Utf32 + Iconv.iconv( 'UTF-32', from_enc, str )[0] + when :Utf16 + Iconv.iconv( 'UTF-16', from_enc, str )[0] + when :Utf8 + Iconv.iconv( 'UTF-8', from_enc, str )[0] + when :None + str + else + raise YAML::Error, ERROR_UNSUPPORTED_ENCODING % to_enc.inspect + end + end + + def YAML.sniff_encoding( str ) + unless YAML::unicode + :None + else + case str + when /^\x00\x00\xFE\xFF/ # UTF-32 + :Utf32 + when /^\xFE\xFF/ # UTF-32BE + :Utf16 + else + :Utf8 + end + end + end + + def YAML.enc_separator( enc ) + case enc + when :Utf32 + "\000\000\000\n" + when :Utf16 + "\000\n" + when :Utf8 + "\n" + when :None + "\n" + else + raise YAML::Error, ERROR_UNSUPPORTED_ENCODING % enc.inspect + end + end + + # + # Escape the string, condensing common escapes + # + def YAML.escape( value ) + value.gsub( /\\/, "\\\\\\" ).gsub( /"/, "\\\"" ).gsub( /([\x00-\x1f])/ ) { |x| ESCAPES[ x.unpack("C")[0] ] } + end + + # + # Unescape the condenses escapes + # + def YAML.unescape( value ) + value.gsub( /\\(?:([nevbr\\fartz])|0?x([0-9a-fA-F]{2})|u([0-9a-fA-F]{4}))/ ) { |x| + if $3 + ["#$3".hex ].pack('U*') + elsif $2 + [$2].pack( "H2" ) + else + UNESCAPES[$1] + end + } + end + +end diff --git a/lib/yaml/error.rb b/lib/yaml/error.rb new file mode 100644 index 0000000000..a9df22749b --- /dev/null +++ b/lib/yaml/error.rb @@ -0,0 +1,33 @@ +# +# Error messages and exception class +# + +module YAML + + # + # Error messages + # + + ERROR_NO_HEADER_NODE = "With UseHeader=false, the node Array or Hash must have elements" + ERROR_NEED_HEADER = "With UseHeader=false, the node must be an Array or Hash" + ERROR_BAD_EXPLICIT = "Unsupported explicit transfer: '%s'" + ERROR_MANY_EXPLICIT = "More than one explicit transfer" + ERROR_MANY_IMPLICIT = "More than one implicit request" + ERROR_NO_ANCHOR = "No anchor for alias '%s'" + ERROR_BAD_ANCHOR = "Invalid anchor: %s" + ERROR_MANY_ANCHOR = "More than one anchor" + ERROR_ANCHOR_ALIAS = "Can't define both an anchor and an alias" + ERROR_BAD_ALIAS = "Invalid alias: %s" + ERROR_MANY_ALIAS = "More than one alias" + ERROR_ZERO_INDENT = "Can't use zero as an indentation width" + ERROR_UNSUPPORTED_VERSION = "This release of YAML.rb does not support YAML version %s" + ERROR_UNSUPPORTED_ENCODING = "Attempt to use unsupported encoding: %s" + + # + # YAML Error classes + # + + class Error < StandardError; end + class ParseError < Error; end + +end diff --git a/lib/yaml/rubytypes.rb b/lib/yaml/rubytypes.rb new file mode 100644 index 0000000000..b6f1e37705 --- /dev/null +++ b/lib/yaml/rubytypes.rb @@ -0,0 +1,558 @@ +require 'date' +# +# Type conversions +# +class Object + def is_complex_yaml? + true + end + def to_yaml_type + "!ruby/object:#{self.class}" + end + def to_yaml_properties + instance_variables.sort + end + def to_yaml( opts = {} ) + YAML::quick_emit( self.id, opts ) { |out| + out.map( self.to_yaml_type ) { |map| + to_yaml_properties.each { |m| + map.add( m[1..-1], instance_eval( m ) ) + } + } + } + end +end + +YAML.add_ruby_type( Object ) { |type, val| + type, obj_class = YAML.read_type_class( type, Object ) + YAML.object_maker( obj_class, val ) +} + +# +# Maps: Hash#to_yaml +# +class Hash + def is_complex_yaml? + true + end + def to_yaml_type + if self.class == Hash or self.class == YAML::SpecialHash + "!map" + else + "!ruby/hash:#{self.class}" + end + end + def to_yaml( opts = {} ) + opts[:DocType] = self.class if Hash === opts + YAML::quick_emit( self.id, opts ) { |out| + hash_type = to_yaml_type + if not out.options[:ExplicitTypes] and hash_type == "!map" + hash_type = "" + end + out.map( hash_type ) { |map| + # + # Sort the hash + # + if out.options[:SortKeys] + map.concat( self.sort ) + else + map.concat( self.to_a ) + end + } + } + end +end + +hash_proc = Proc.new { |type, val| + if Array === val + val = Hash.[]( *val ) # Convert the map to a sequence + elsif Hash === val + type, obj_class = YAML.read_type_class( type, Hash ) + if obj_class != Hash + o = obj_class.new + o.update( val ) + val = o + end + else + raise YAML::Error, "Invalid map explicitly tagged !map: " + val.inspect + end + val +} +YAML.add_builtin_type( /^map/, &hash_proc ) +YAML.add_ruby_type( Hash, &hash_proc ) + +module YAML + + # + # Ruby-specific collection: !ruby/flexhash + # + class FlexHash < Array + def []( k ) + self.assoc( k ).to_a[1] + end + def []=( k, *rest ) + val, set = rest.reverse + if ( tmp = self.assoc( k ) ) and not set + tmp[1] = val + else + self << [ k, val ] + end + val + end + def has_key?( k ) + self.assoc( k ) ? true : false + end + def is_complex_yaml? + true + end + def to_yaml( opts = {} ) + YAML::quick_emit( self.id, opts ) { |out| + out.seq( "!ruby/flexhash" ) { |seq| + self.each { |v| + if v[1] + seq.add( Hash.[]( *v ) ) + else + seq.add( v[0] ) + end + } + } + } + end + end + + YAML.add_ruby_type( :flexhash ) { |type, val| + if Array === val + p = FlexHash.new + val.each { |v| + if Hash === v + p.concat( v.to_a ) # Convert the map to a sequence + else + p << [ v, nil ] + end + } + p + else + raise YAML::Error, "Invalid !ruby/flexhash: " + val.inspect + end + } +end + +# +# Structs: export as a !map +# +class Struct + def is_complex_yaml? + true + end + def to_yaml( opts = {} ) + YAML::quick_emit( self.id, opts ) { |out| + # + # Basic struct is passed as a YAML map + # + struct_name = self.class.name.gsub( "Struct:", "" ) + out.map( "!ruby/struct#{struct_name}" ) { |map| + self.members.each { |m| + map.add( m, self[m] ) + } + } + } + end +end + +YAML.add_ruby_type( Struct ) { |type, val| + type =~ /^struct:(\w+)/ + if Hash === val + type = $1 + struct_type = nil + struct_def = [] + struct_name = "" + if $1.to_s.length > 1 + struct_name = $1[0..$1.length] + struct_def << struct_name + end + + # + # Use existing Struct if it exists + # + begin + struct_type = Struct.const_get( struct_name ) + rescue NameError + end + if not struct_type + struct_type = Struct.new( *struct_def.concat( val.keys.collect { |k| k.intern } ) ) + end + + # + # Set the Struct properties + # + st = struct_type.new + st.members.each { |m| + st.send( "#{m}=", val[m] ) + } + st + else + raise YAML::Error, "Invalid Ruby Struct: " + val.inspect + end +} + +# +# Sequences: Array#to_yaml +# +class Array + def is_complex_yaml? + true + end + def to_yaml_type + if self.class == Array + "!seq" + else + "!ruby/array:#{self.class}" + end + end + def to_yaml( opts = {} ) + opts[:DocType] = self.class if Hash === opts + YAML::quick_emit( self.id, opts ) { |out| + array_type = to_yaml_type + if not out.options[:ExplicitTypes] and array_type == "!seq" + array_type = "" + end + + out.seq( array_type ) { |seq| + seq.concat( self ) + } + } + end +end + +array_proc = Proc.new { |type, val| + if Array === val + type, obj_class = YAML.read_type_class( type, Array ) + if obj_class != Array + o = obj_class.new + o.concat( val ) + val = o + end + val + else + val.to_a + end +} +YAML.add_builtin_type( /^seq/, &array_proc ) +YAML.add_ruby_type( Array, &array_proc ) + +# +# String#to_yaml +# +class String + def is_complex_yaml? + ( self =~ /\n.+/ ? true : false ) + end + def is_binary_data? + ( self.count( "^ -~", "^\r\n" ) / self.size > 0.3 || self.count( "\x00" ) > 0 ) + end + def to_yaml( opts = {} ) + complex = false + if self.is_complex_yaml? + complex = true + elsif opts[:BestWidth].to_i > 0 + if self.length > opts[:BestWidth] and opts[:UseFold] + complex = true + end + end + YAML::quick_emit( complex ? self.id : nil, opts ) { |out| + if complex + if self.is_binary_data? + out.binary_base64( self ) + else + out.node_text( self ) + end + else + ostr = if out.options[:KeepValue] + self + elsif empty? + "''" + elsif YAML.detect_implicit( self ) != 'str' + "\"#{YAML.escape( self )}\"" + elsif self =~ /#{YAML::ESCAPE_CHAR}|[#{YAML::SPACE_INDICATORS}] |\n|\'/ + "\"#{YAML.escape( self )}\"" + elsif self =~ /^[^#{YAML::WORD_CHAR}]/ + "\"#{YAML.escape( self )}\"" + else + self + end + out.simple( ostr ) + end + } + end +end + +YAML.add_builtin_type( 'str' ) { |type,val| val.to_s } +YAML.add_builtin_type( 'binary' ) { |type,val| + enctype = "m" + if String === val + val.gsub( /\s+/, '' ).unpack( enctype )[0] + else + raise YAML::Error, "Binary data must be represented by a string: " + val.inspect + end +} + +# +# Symbol#to_yaml +# +class Symbol + def is_complex_yaml? + false + end + def to_yaml( opts = {} ) + YAML::quick_emit( nil, opts ) { |out| + out << "!ruby/sym " + self.id2name.to_yaml( :Emitter => out ) + } + end +end + +symbol_proc = Proc.new { |type, val| + if String === val + val.intern + else + raise YAML::Error, "Invalid Symbol: " + val.inspect + end +} +YAML.add_ruby_type( Symbol, &symbol_proc ) +YAML.add_ruby_type( :sym, &symbol_proc ) + +# +# Range#to_yaml +# +class Range + def is_complex_yaml? + false + end + def to_yaml( opts = {} ) + YAML::quick_emit( nil, opts ) { |out| + out << "!ruby/range " + self.inspect.to_yaml( :Emitter => out ) + } + end +end + +YAML.add_ruby_type( Range ) { |type, val| + if String === val and val =~ /^(.*[^.])(\.{2,3})([^.].*)$/ + r1, rdots, r2 = $1, $2, $3 + Range.new( YAML.try_implicit( r1 ), YAML.try_implicit( r2 ), rdots.length == 3 ) + elsif Hash === val + Range.new( val['begin'], val['end'], val['exclude_end?'] ) + else + raise YAML::Error, "Invalid Range: " + val.inspect + end +} + +# +# Make an RegExp +# +class Regexp + def is_complex_yaml? + false + end + def to_yaml( opts = {} ) + YAML::quick_emit( nil, opts ) { |out| + out << "!ruby/regexp " + self.inspect.to_yaml( :Emitter => out ) + } + end +end + +regexp_proc = Proc.new { |type, val| + if String === val and val =~ /^\/(.*)\/([mix]*)$/ + val = { 'REGEXP' => $1, 'MODIFIERS' => $2 } + end + if Hash === val + mods = nil + unless val['MODIFIERS'].to_s.empty? + mods = 0x00 + if val['MODIFIERS'].include?( 'x' ) + mods |= Regexp::EXTENDED + elsif val['MODIFIERS'].include?( 'i' ) + mods |= Regexp::IGNORECASE + elsif val['MODIFIERS'].include?( 'm' ) + mods |= Regexp::POSIXLINE + end + end + Regexp::compile( val['REGEXP'], mods ) + else + raise YAML::Error, "Invalid Regular expression: " + val.inspect + end +} +YAML.add_domain_type( "perl.yaml.org,2002", /^regexp/, ®exp_proc ) +YAML.add_ruby_type( Regexp, ®exp_proc ) + +# +# Emit a Time object as an ISO 8601 timestamp +# +class Time + def is_complex_yaml? + false + end + def to_yaml( opts = {} ) + YAML::quick_emit( nil, opts ) { |out| + tz = "Z" + # from the tidy Tobias Peters Thanks! + unless self.utc? + utc_same_instant = self.dup.utc + utc_same_writing = Time.utc(year,month,day,hour,min,sec,usec) + difference_to_utc = utc_same_writing - utc_same_instant + if (difference_to_utc < 0) + difference_sign = '-' + absolute_difference = -difference_to_utc + else + difference_sign = '+' + absolute_difference = difference_to_utc + end + difference_minutes = (absolute_difference/60).round + tz = "%s%02d:%02d" % [ difference_sign, difference_minutes / 60, difference_minutes % 60] + end + ( self.strftime( "%Y-%m-%d %H:%M:%S." ) + + "%06d %s" % [usec, tz] ). + to_yaml( :Emitter => out, :KeepValue => true ) + } + end +end + +YAML.add_builtin_type( 'time' ) { |type, val| + if val =~ /\A(\d{4})\-(\d{1,2})\-(\d{1,2})[Tt](\d{2})\:(\d{2})\:(\d{2})(\.\d{1,2})?(Z|[-+][0-9][0-9](?:\:[0-9][0-9])?)\Z/ + YAML.mktime( *$~.to_a[1,8] ) + elsif val =~ /\A(\d{4})\-(\d{1,2})\-(\d{1,2})[ \t]+(\d{2})\:(\d{2})\:(\d{2})(\.\d+)?[ \t]+(Z|[-+][0-9][0-9](?:\:[0-9][0-9])?)\Z/ + YAML.mktime( *$~.to_a[1,8] ) + elsif val =~ /\A(\d{4})\-(\d{1,2})\-(\d{1,2})[ \t]+(\d{2})\:(\d{2})\:(\d{2})(\.\d{1,2})?\Z/ + YAML.mktime( *$~.to_a[1,7] ) + elsif val =~ /\A(\d{4})\-(\d{1,2})\-(\d{1,2})\Z/ + Date.new($1.to_i, $2.to_i, $3.to_i) + elsif type == :Implicit + :InvalidType + else + raise YAML::TypeError, "Invalid !time string: " + val.inspect + end +} + +# +# Emit a Date object as a simple implicit +# +class Date + def is_complex_yaml? + false + end + def to_yaml( opts = {} ) + opts[:KeepValue] = true + self.to_s.to_yaml( opts ) + end +end + +# +# Send Integer, Booleans, NilClass to String +# +class Numeric + def is_complex_yaml? + false + end + def to_yaml( opts = {} ) + str = self.to_s + if str == "Infinity" + str = ".Inf" + elsif str == "-Infinity" + str = "-.Inf" + elsif str == "NaN" + str = ".NaN" + end + opts[:KeepValue] = true + str.to_yaml( opts ) + end +end + +YAML.add_builtin_type( 'float' ) { |type, val| + if val =~ /\A[-+]?[\d][\d,]*\.[\d,]*[eE][-+][0-9]+\Z/ # Float (exponential) + $&.tr( ',', '' ).to_f + elsif val =~ /\A[-+]?[\d][\d,]*\.[\d,]*\Z/ # Float (fixed) + $&.tr( ',', '' ).to_f + elsif val =~ /\A([-+]?)\.(inf|Inf|INF)\Z/ # Float (english) + ( $1 == "-" ? -1.0/0.0 : 1.0/0.0 ) + elsif val =~ /\A\.(nan|NaN|NAN)\Z/ + 0.0/0.0 + elsif type == :Implicit + :InvalidType + else + val.to_f + end +} + +YAML.add_builtin_type( 'int' ) { |type, val| + if val =~ /\A[-+]?0[0-7,]+\Z/ # Integer (octal) + $&.oct + elsif val =~ /\A[-+]?0x[0-9a-fA-F,]+\Z/ # Integer (hex) + $&.hex + elsif val =~ /\A[-+]?\d[\d,]*\Z/ # Integer (canonical) + $&.tr( ',', '' ).to_i + elsif val =~ /\A([-+]?)(\d[\d,]*(?::[0-5]?[0-9])+)\Z/ + sign = ( $1 == '-' ? -1 : 1 ) + digits = $2.split( /:/ ).collect { |x| x.to_i } + val = 0; digits.each { |x| val = ( val * 60 ) + x }; val *= sign + elsif type == :Implicit + :InvalidType + else + val.to_i + end +} + +class TrueClass + def is_complex_yaml? + false + end + def to_yaml( opts = {} ) + opts[:KeepValue] = true + "true".to_yaml( opts ) + end +end + +class FalseClass + def is_complex_yaml? + false + end + def to_yaml( opts = {} ) + opts[:KeepValue] = true + "false".to_yaml( opts ) + end +end + +YAML.add_builtin_type( 'bool' ) { |type, val| + if val =~ /\A(\+|true|True|TRUE|yes|Yes|YES|on|On|ON)\Z/ + true + elsif val =~ /\A(\-|false|False|FALSE|no|No|NO|off|Off|OFF)\Z/ + false + elsif type == :Implicit + :InvalidType + else + raise YAML::TypeError, "Invalid !bool string: " + val.inspect + end +} + +class NilClass + def is_complex_yaml? + false + end + def to_yaml( opts = {} ) + opts[:KeepValue] = true + "".to_yaml( opts ) + end +end + +YAML.add_builtin_type( 'null' ) { |type, val| + if val =~ /\A(\~|null|Null|NULL)\Z/ + nil + elsif val.empty? + nil + elsif type == :Implicit + :InvalidType + else + raise YAML::TypeError, "Invalid !null string: " + val.inspect + end +} + diff --git a/lib/yaml/store.rb b/lib/yaml/store.rb new file mode 100644 index 0000000000..b2924b0660 --- /dev/null +++ b/lib/yaml/store.rb @@ -0,0 +1,75 @@ +# +# YAML::Store +# +require 'yaml' +require 'pstore' + +module YAML + + class Store < PStore + # + # Constructor + # + def initialize( *o ) + @opt = YAML::DEFAULTS.dup + if String === o.first + super(o.pop) + end + if o.last.is_a? Hash + @opt.update(o.pop) + end + end + + # + # Override Pstore#transaction + # + def transaction + raise YAML::Error, "nested transaction" if @transaction + raise YAML::Error, "no filename for transaction" unless @filename + begin + @transaction = true + value = nil + backup = @filename+"~" + if File::exist?(@filename) + file = File::open(@filename, "rb+") + orig = true + else + @table = {} + file = File::open(@filename, "wb+") + file.write( @table.to_yaml( @opt ) ) + end + file.flock(File::LOCK_EX) + if orig + File::copy @filename, backup + @table = YAML::load( file ) + end + begin + catch(:pstore_abort_transaction) do + value = yield(self) + end + rescue Exception + @abort = true + raise + ensure + unless @abort + begin + file.rewind + file.write( @table.to_yaml( @opt ) ) + file.truncate(file.pos) + rescue + File::rename backup, @filename if File::exist?(backup) + raise + end + end + @abort = false + end + ensure + @table = nil + @transaction = false + file.close if file + end + value + end + end + +end diff --git a/lib/yaml/stream.rb b/lib/yaml/stream.rb new file mode 100644 index 0000000000..3b6919b5dd --- /dev/null +++ b/lib/yaml/stream.rb @@ -0,0 +1,44 @@ +module YAML + + # + # YAML::Stream -- for emitting many documents + # + class Stream + + attr_accessor :documents, :options + + def initialize( opts = {} ) + @options = opts + @documents = [] + end + + def []( i ) + @documents[ i ] + end + + def add( doc ) + @documents << doc + end + + def edit( doc_num, doc ) + @documents[ doc_num ] = doc + end + + def emit + opts = @options.dup + opts[:UseHeader] = true if @documents.length > 1 + ct = 0 + out = Emitter.new( opts ) + @documents.each { |v| + if ct > 0 + out << "\n--- " + end + v.to_yaml( :Emitter => out ) + ct += 1 + } + out.end_object + end + + end + +end diff --git a/lib/yaml/stringio.rb b/lib/yaml/stringio.rb new file mode 100644 index 0000000000..8ad949fa2b --- /dev/null +++ b/lib/yaml/stringio.rb @@ -0,0 +1,83 @@ +# +# Limited StringIO if no core lib is available +# +begin +require 'stringio' +rescue LoadError + # StringIO based on code by MoonWolf + class StringIO + def initialize(string="") + @string=string + @pos=0 + @eof=(string.size==0) + end + def pos + @pos + end + def eof + @eof + end + alias eof? eof + def readline(rs=$/) + if @eof + raise EOFError + else + if p = @string[@pos..-1]=~rs + line = @string[@pos,p+1] + else + line = @string[@pos..-1] + end + @pos+=line.size + @eof =true if @pos==@string.size + $_ = line + end + end + def rewind + seek(0,0) + end + def seek(offset,whence) + case whence + when 0 + @pos=offset + when 1 + @pos+=offset + when 2 + @pos=@string.size+offset + end + @eof=(@pos>=@string.size) + 0 + end + end + + # + # Class method for creating streams + # + def YAML.make_stream( io ) + if String === io + io = StringIO.new( io ) + elsif not IO === io + raise YAML::Error, "YAML stream must be an IO or String object." + end + if YAML::unicode + def io.readline + YAML.utf_to_internal( readline( @ln_sep ), @utf_encoding ) + end + def io.check_unicode + @utf_encoding = YAML.sniff_encoding( read( 4 ) ) + @ln_sep = YAML.enc_separator( @utf_encoding ) + seek( -4, IO::SEEK_CUR ) + end + def io.utf_encoding + @utf_encoding + end + io.check_unicode + else + def io.utf_encoding + :None + end + end + io + end + +end + diff --git a/lib/yaml/syck.rb b/lib/yaml/syck.rb new file mode 100644 index 0000000000..faf57e8036 --- /dev/null +++ b/lib/yaml/syck.rb @@ -0,0 +1,19 @@ +# +# YAML::Syck module +# .. glues syck and yaml.rb together .. +# +require 'syck' +require 'yaml/basenode' + +module YAML + module Syck + + # + # Mixin BaseNode functionality + # + class Node + include YAML::BaseNode + end + + end +end diff --git a/lib/yaml/types.rb b/lib/yaml/types.rb new file mode 100644 index 0000000000..7602c436e7 --- /dev/null +++ b/lib/yaml/types.rb @@ -0,0 +1,195 @@ +# +# Classes required by the full core typeset +# +module YAML + + # + # Default private type + # + class PrivateType + attr_accessor :type_id, :value + def initialize( type, val ) + @type_id = type; @value = val + end + def to_yaml( opts = {} ) + YAML::quick_emit( self.id, opts ) { |out| + out << " !!#{@type_id}" + value.to_yaml( :Emitter => out ) + } + end + end + + # + # Default domain type + # + class DomainType + attr_accessor :domain, :type_id, :value + def initialize( domain, type, val ) + @domain = domain; @type_id = type; @value = val + end + def to_yaml_type + dom = @domain.dup + if dom =~ /\.yaml\.org,2002$/ + dom = $` + end + "#{dom}/#{@type_id}" + end + def to_yaml( opts = {} ) + YAML::quick_emit( self.id, opts ) { |out| + out << " !#{to_yaml_type} " + value.to_yaml( :Emitter => out ) + } + end + end + + # + # YAML Hash class to support comments and defaults + # + class SpecialHash < Kernel::Hash + attr_accessor :default + def inspect + self.default.to_s + end + def to_s + self.default.to_s + end + def update( h ) + if YAML::SpecialHash === h + @default = h.default if h.default + end + super( h ) + end + def to_yaml( opts = {} ) + opts[:DefaultKey] = self.default + super( opts ) + end + end + + # + # Builtin collection: !omap + # + class Omap < Array + def self.[]( *vals ) + o = Omap.new + 0.step( vals.length - 1, 2 ) { |i| + o[vals[i]] = vals[i+1] + } + o + end + def []( k ) + self.assoc( k ).to_a[1] + end + def []=( k, *rest ) + val, set = rest.reverse + if ( tmp = self.assoc( k ) ) and not set + tmp[1] = val + else + self << [ k, val ] + end + val + end + def has_key?( k ) + self.assoc( k ) ? true : false + end + def is_complex_yaml? + true + end + def to_yaml( opts = {} ) + YAML::quick_emit( self.id, opts ) { |out| + out.seq( "!omap" ) { |seq| + self.each { |v| + seq.add( Hash[ *v ] ) + } + } + } + end + end + + YAML.add_builtin_type( "omap" ) { |type, val| + if Array === val + p = Omap.new + val.each { |v| + if Hash === v + p.concat( v.to_a ) # Convert the map to a sequence + else + raise YAML::Error, "Invalid !omap entry: " + val.inspect + end + } + else + raise YAML::Error, "Invalid !omap: " + val.inspect + end + p + } + + # + # Builtin collection: !pairs + # + class Pairs < Array + def self.[]( *vals ) + p = Pairs.new + 0.step( vals.length - 1, 2 ) { |i| + p[vals[i]] = vals[i+1] + } + p + end + def []( k ) + self.assoc( k ).to_a + end + def []=( k, val ) + self << [ k, val ] + val + end + def has_key?( k ) + self.assoc( k ) ? true : false + end + def is_complex_yaml? + true + end + def to_yaml( opts = {} ) + YAML::quick_emit( self.id, opts ) { |out| + out.seq( "!pairs" ) { |seq| + self.each { |v| + seq.add( Hash[ *v ] ) + } + } + } + end + end + + YAML.add_builtin_type( "pairs" ) { |type, val| + if Array === val + p = Pairs.new + val.each { |v| + if Hash === v + p.concat( v.to_a ) # Convert the map to a sequence + else + raise YAML::Error, "Invalid !pairs entry: " + val.inspect + end + } + else + raise YAML::Error, "Invalid !pairs: " + val.inspect + end + p + } + + # + # Builtin collection: !set + # + class Set < Hash + def to_yaml_type + "!set" + end + end + + YAML.add_builtin_type( 'set' ) { |type, val| + if Array === val + val = Set[ *val ] + elsif Hash === val + Set[ val ] + else + raise YAML::Error, "Invalid map explicitly tagged !map: " + val.inspect + end + val + } + +end diff --git a/lib/yaml/yamlnode.rb b/lib/yaml/yamlnode.rb new file mode 100644 index 0000000000..e36a18e694 --- /dev/null +++ b/lib/yaml/yamlnode.rb @@ -0,0 +1,54 @@ +# +# YAML::YamlNode class +# +require 'yaml/basenode' + +module YAML + + # + # YAML Generic Model container + # + class YamlNode + include BaseNode + attr_accessor :kind, :type_id, :value, :anchor + def initialize( t, v ) + @type_id = t + if Hash === v + @kind = 'map' + @value = {} + v.each { |k,v| + @value[ k.transform ] = [ k, v ] + } + elsif Array === v + @kind = 'seq' + @value = v + elsif String === v + @kind = 'scalar' + @value = v + end + end + + # + # Transform this node fully into a native type + # + def transform + t = nil + if @value.is_a? Hash + t = {} + @value.each { |k,v| + t[ k ] = v[1].transform + } + elsif @value.is_a? Array + t = [] + @value.each { |v| + t.push v.transform + } + else + t = @value + end + YAML.transfer_method( @type_id, t ) + end + + end + +end diff --git a/lib/yaml/ypath.rb b/lib/yaml/ypath.rb new file mode 100644 index 0000000000..713c4de76e --- /dev/null +++ b/lib/yaml/ypath.rb @@ -0,0 +1,52 @@ +# +# YAML::YPath +# + +module YAML + + class YPath + attr_accessor :segments, :predicates, :flags + def initialize( str ) + @segments = [] + @predicates = [] + @flags = nil + while str =~ /^\/?(\/|[^\/[]+)(?:\[([^\]]+)\])?/ + @segments.push $1 + @predicates.push $2 + str = $' + end + unless str.to_s.empty? + @segments += str.split( "/" ) + end + if @segments.length == 0 + @segments.push "." + end + end + def YPath.each_path( str ) + # + # Find choices + # + paths = [] + str = "(#{ str })" + while str.sub!( /\(([^()]+)\)/, "\n#{ paths.length }\n" ) + paths.push $1.split( '|' ) + end + + # + # Construct all possible paths + # + all = [ str ] + ( paths.length - 1 ).downto( 0 ) do |i| + all = all.collect do |a| + paths[i].collect do |p| + a.gsub( /\n#{ i }\n/, p ) + end + end.flatten.uniq + end + all.collect do |path| + yield YPath.new( path ) + end + end + end + +end -- cgit v1.2.3