diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/json.rb | 10 | ||||
-rw-r--r-- | lib/json/Array.xpm | 21 | ||||
-rw-r--r-- | lib/json/FalseClass.xpm | 21 | ||||
-rw-r--r-- | lib/json/Hash.xpm | 21 | ||||
-rw-r--r-- | lib/json/Key.xpm | 73 | ||||
-rw-r--r-- | lib/json/NilClass.xpm | 21 | ||||
-rw-r--r-- | lib/json/Numeric.xpm | 28 | ||||
-rw-r--r-- | lib/json/String.xpm | 96 | ||||
-rw-r--r-- | lib/json/TrueClass.xpm | 21 | ||||
-rw-r--r-- | lib/json/add/core.rb | 135 | ||||
-rw-r--r-- | lib/json/add/rails.rb | 58 | ||||
-rw-r--r-- | lib/json/common.rb | 356 | ||||
-rw-r--r-- | lib/json/editor.rb | 1371 | ||||
-rw-r--r-- | lib/json/ext.rb | 15 | ||||
-rw-r--r-- | lib/json/ext/.keep | 0 | ||||
-rw-r--r-- | lib/json/json.xpm | 1499 | ||||
-rw-r--r-- | lib/json/pure.rb | 77 | ||||
-rw-r--r-- | lib/json/pure/generator.rb | 429 | ||||
-rw-r--r-- | lib/json/pure/parser.rb | 269 | ||||
-rw-r--r-- | lib/json/version.rb | 8 |
20 files changed, 4529 insertions, 0 deletions
diff --git a/lib/json.rb b/lib/json.rb new file mode 100644 index 0000000..789b0de --- /dev/null +++ b/lib/json.rb @@ -0,0 +1,10 @@ +require 'json/common' +module JSON + require 'json/version' + + begin + require 'json/ext' + rescue LoadError + require 'json/pure' + end +end diff --git a/lib/json/Array.xpm b/lib/json/Array.xpm new file mode 100644 index 0000000..27c4801 --- /dev/null +++ b/lib/json/Array.xpm @@ -0,0 +1,21 @@ +/* XPM */ +static char * Array_xpm[] = { +"16 16 2 1", +" c None", +". c #000000", +" ", +" ", +" ", +" .......... ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" .......... ", +" ", +" ", +" "}; diff --git a/lib/json/FalseClass.xpm b/lib/json/FalseClass.xpm new file mode 100644 index 0000000..25ce608 --- /dev/null +++ b/lib/json/FalseClass.xpm @@ -0,0 +1,21 @@ +/* XPM */ +static char * False_xpm[] = { +"16 16 2 1", +" c None", +". c #FF0000", +" ", +" ", +" ", +" ...... ", +" . ", +" . ", +" . ", +" ...... ", +" . ", +" . ", +" . ", +" . ", +" . ", +" ", +" ", +" "}; diff --git a/lib/json/Hash.xpm b/lib/json/Hash.xpm new file mode 100644 index 0000000..cd8f6f7 --- /dev/null +++ b/lib/json/Hash.xpm @@ -0,0 +1,21 @@ +/* XPM */ +static char * Hash_xpm[] = { +"16 16 2 1", +" c None", +". c #000000", +" ", +" ", +" ", +" . . ", +" . . ", +" . . ", +" ......... ", +" . . ", +" . . ", +" ......... ", +" . . ", +" . . ", +" . . ", +" ", +" ", +" "}; diff --git a/lib/json/Key.xpm b/lib/json/Key.xpm new file mode 100644 index 0000000..9fd7281 --- /dev/null +++ b/lib/json/Key.xpm @@ -0,0 +1,73 @@ +/* XPM */ +static char * Key_xpm[] = { +"16 16 54 1", +" c None", +". c #110007", +"+ c #0E0900", +"@ c #000013", +"# c #070600", +"$ c #F6F006", +"% c #ECE711", +"& c #E5EE00", +"* c #16021E", +"= c #120900", +"- c #EDF12B", +"; c #000033", +"> c #0F0000", +", c #FFFE03", +"' c #E6E500", +") c #16021B", +"! c #F7F502", +"~ c #000E00", +"{ c #130000", +"] c #FFF000", +"^ c #FFE711", +"/ c #140005", +"( c #190025", +"_ c #E9DD27", +": c #E7DC04", +"< c #FFEC09", +"[ c #FFE707", +"} c #FFDE10", +"| c #150021", +"1 c #160700", +"2 c #FAF60E", +"3 c #EFE301", +"4 c #FEF300", +"5 c #E7E000", +"6 c #FFFF08", +"7 c #0E0206", +"8 c #040000", +"9 c #03052E", +"0 c #041212", +"a c #070300", +"b c #F2E713", +"c c #F9DE13", +"d c #36091E", +"e c #00001C", +"f c #1F0010", +"g c #FFF500", +"h c #DEDE00", +"i c #050A00", +"j c #FAF14A", +"k c #F5F200", +"l c #040404", +"m c #1A0D00", +"n c #EDE43D", +"o c #ECE007", +" ", +" ", +" .+@ ", +" #$%&* ", +" =-;>,') ", +" >!~{]^/ ", +" (_:<[}| ", +" 1234567 ", +" 890abcd ", +" efghi ", +" >jkl ", +" mnol ", +" >kl ", +" ll ", +" ", +" "}; diff --git a/lib/json/NilClass.xpm b/lib/json/NilClass.xpm new file mode 100644 index 0000000..3509f06 --- /dev/null +++ b/lib/json/NilClass.xpm @@ -0,0 +1,21 @@ +/* XPM */ +static char * False_xpm[] = { +"16 16 2 1", +" c None", +". c #000000", +" ", +" ", +" ", +" ... ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" . . ", +" ... ", +" ", +" ", +" "}; diff --git a/lib/json/Numeric.xpm b/lib/json/Numeric.xpm new file mode 100644 index 0000000..e071e2e --- /dev/null +++ b/lib/json/Numeric.xpm @@ -0,0 +1,28 @@ +/* XPM */ +static char * Numeric_xpm[] = { +"16 16 9 1", +" c None", +". c #FF0000", +"+ c #0000FF", +"@ c #0023DB", +"# c #00EA14", +"$ c #00FF00", +"% c #004FAF", +"& c #0028D6", +"* c #00F20C", +" ", +" ", +" ", +" ... +++@#$$$$ ", +" .+ %& $$ ", +" . + $ ", +" . + $$ ", +" . ++$$$$ ", +" . + $$ ", +" . + $ ", +" . + $ ", +" . + $ $$ ", +" .....++++*$$ ", +" ", +" ", +" "}; diff --git a/lib/json/String.xpm b/lib/json/String.xpm new file mode 100644 index 0000000..f79a89c --- /dev/null +++ b/lib/json/String.xpm @@ -0,0 +1,96 @@ +/* XPM */ +static char * String_xpm[] = { +"16 16 77 1", +" c None", +". c #000000", +"+ c #040404", +"@ c #080806", +"# c #090606", +"$ c #EEEAE1", +"% c #E7E3DA", +"& c #E0DBD1", +"* c #D4B46F", +"= c #0C0906", +"- c #E3C072", +"; c #E4C072", +"> c #060505", +", c #0B0A08", +"' c #D5B264", +") c #D3AF5A", +"! c #080602", +"~ c #E1B863", +"{ c #DDB151", +"] c #DBAE4A", +"^ c #DDB152", +"/ c #DDB252", +"( c #070705", +"_ c #0C0A07", +": c #D3A33B", +"< c #020201", +"[ c #DAAA41", +"} c #040302", +"| c #E4D9BF", +"1 c #0B0907", +"2 c #030201", +"3 c #020200", +"4 c #C99115", +"5 c #080704", +"6 c #DBC8A2", +"7 c #E7D7B4", +"8 c #E0CD9E", +"9 c #080601", +"0 c #040400", +"a c #010100", +"b c #0B0B08", +"c c #DCBF83", +"d c #DCBC75", +"e c #DEB559", +"f c #040301", +"g c #BC8815", +"h c #120E07", +"i c #060402", +"j c #0A0804", +"k c #D4A747", +"l c #D6A12F", +"m c #0E0C05", +"n c #C8C1B0", +"o c #1D1B15", +"p c #D7AD51", +"q c #070502", +"r c #080804", +"s c #BC953B", +"t c #C4BDAD", +"u c #0B0807", +"v c #DBAC47", +"w c #1B150A", +"x c #B78A2C", +"y c #D8A83C", +"z c #D4A338", +"A c #0F0B03", +"B c #181105", +"C c #C59325", +"D c #C18E1F", +"E c #060600", +"F c #CC992D", +"G c #B98B25", +"H c #B3831F", +"I c #C08C1C", +"J c #060500", +"K c #0E0C03", +"L c #0D0A00", +" ", +" .+@# ", +" .$%&*= ", +" .-;>,')! ", +" .~. .{]. ", +" .^/. (_:< ", +" .[.}|$12 ", +" 345678}90 ", +" a2bcdefgh ", +" ijkl.mno ", +" <pq. rstu ", +" .]v. wx= ", +" .yzABCDE ", +" .FGHIJ ", +" 0KL0 ", +" "}; diff --git a/lib/json/TrueClass.xpm b/lib/json/TrueClass.xpm new file mode 100644 index 0000000..143eef4 --- /dev/null +++ b/lib/json/TrueClass.xpm @@ -0,0 +1,21 @@ +/* XPM */ +static char * TrueClass_xpm[] = { +"16 16 2 1", +" c None", +". c #0BF311", +" ", +" ", +" ", +" ......... ", +" . ", +" . ", +" . ", +" . ", +" . ", +" . ", +" . ", +" . ", +" . ", +" ", +" ", +" "}; diff --git a/lib/json/add/core.rb b/lib/json/add/core.rb new file mode 100644 index 0000000..4423e7a --- /dev/null +++ b/lib/json/add/core.rb @@ -0,0 +1,135 @@ +# This file contains implementations of ruby core's custom objects for +# serialisation/deserialisation. + +unless Object.const_defined?(:JSON) and ::JSON.const_defined?(:JSON_LOADED) and + ::JSON::JSON_LOADED + require 'json' +end +require 'date' + +class Time + def self.json_create(object) + if usec = object.delete('u') # used to be tv_usec -> tv_nsec + object['n'] = usec * 1000 + end + if respond_to?(:tv_nsec) + at(*object.values_at('s', 'n')) + else + at(object['s'], object['n'] / 1000) + end + end + + def to_json(*args) + { + 'json_class' => self.class.name, + 's' => tv_sec, + 'n' => respond_to?(:tv_nsec) ? tv_nsec : tv_usec * 1000 + }.to_json(*args) + end +end + +class Date + def self.json_create(object) + civil(*object.values_at('y', 'm', 'd', 'sg')) + end + + alias start sg unless method_defined?(:start) + + def to_json(*args) + { + 'json_class' => self.class.name, + 'y' => year, + 'm' => month, + 'd' => day, + 'sg' => start, + }.to_json(*args) + end +end + +class DateTime + def self.json_create(object) + args = object.values_at('y', 'm', 'd', 'H', 'M', 'S') + of_a, of_b = object['of'].split('/') + if of_b and of_b != '0' + args << Rational(of_a.to_i, of_b.to_i) + else + args << of_a + end + args << object['sg'] + civil(*args) + end + + alias start sg unless method_defined?(:start) + + def to_json(*args) + { + 'json_class' => self.class.name, + 'y' => year, + 'm' => month, + 'd' => day, + 'H' => hour, + 'M' => min, + 'S' => sec, + 'of' => offset.to_s, + 'sg' => start, + }.to_json(*args) + end +end + +class Range + def self.json_create(object) + new(*object['a']) + end + + def to_json(*args) + { + 'json_class' => self.class.name, + 'a' => [ first, last, exclude_end? ] + }.to_json(*args) + end +end + +class Struct + def self.json_create(object) + new(*object['v']) + end + + def to_json(*args) + klass = self.class.name + klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!" + { + 'json_class' => klass, + 'v' => values, + }.to_json(*args) + end +end + +class Exception + def self.json_create(object) + result = new(object['m']) + result.set_backtrace object['b'] + result + end + + def to_json(*args) + { + 'json_class' => self.class.name, + 'm' => message, + 'b' => backtrace, + }.to_json(*args) + end +end + +class Regexp + def self.json_create(object) + new(object['s'], object['o']) + end + + def to_json(*) + { + 'json_class' => self.class.name, + 'o' => options, + 's' => source, + }.to_json + end +end diff --git a/lib/json/add/rails.rb b/lib/json/add/rails.rb new file mode 100644 index 0000000..e86ed1a --- /dev/null +++ b/lib/json/add/rails.rb @@ -0,0 +1,58 @@ +# This file contains implementations of rails custom objects for +# serialisation/deserialisation. + +unless Object.const_defined?(:JSON) and ::JSON.const_defined?(:JSON_LOADED) and + ::JSON::JSON_LOADED + require 'json' +end + +class Object + def self.json_create(object) + obj = new + for key, value in object + next if key == 'json_class' + instance_variable_set "@#{key}", value + end + obj + end + + def to_json(*a) + result = { + 'json_class' => self.class.name + } + instance_variables.inject(result) do |r, name| + r[name[1..-1]] = instance_variable_get name + r + end + result.to_json(*a) + end +end + +class Symbol + def to_json(*a) + to_s.to_json(*a) + end +end + +module Enumerable + def to_json(*a) + to_a.to_json(*a) + end +end + +# class Regexp +# def to_json(*) +# inspect +# end +# end +# +# The above rails definition has some problems: +# +# 1. { 'foo' => /bar/ }.to_json # => "{foo: /bar/}" +# This isn't valid JSON, because the regular expression syntax is not +# defined in RFC 4627. (And unquoted strings are disallowed there, too.) +# Though it is valid Javascript. +# +# 2. { 'foo' => /bar/mix }.to_json # => "{foo: /bar/mix}" +# This isn't even valid Javascript. + diff --git a/lib/json/common.rb b/lib/json/common.rb new file mode 100644 index 0000000..61548eb --- /dev/null +++ b/lib/json/common.rb @@ -0,0 +1,356 @@ +require 'json/version' + +module JSON + class << self + # If _object_ is string-like parse the string and return the parsed result + # as a Ruby data structure. Otherwise generate a JSON text from the Ruby + # data structure object and return it. + # + # The _opts_ argument is passed through to generate/parse respectively, see + # generate and parse for their documentation. + def [](object, opts = {}) + if object.respond_to? :to_str + JSON.parse(object.to_str, opts => {}) + else + JSON.generate(object, opts => {}) + end + end + + # Returns the JSON parser class, that is used by JSON. This might be either + # JSON::Ext::Parser or JSON::Pure::Parser. + attr_reader :parser + + # Set the JSON parser class _parser_ to be used by JSON. + def parser=(parser) # :nodoc: + @parser = parser + remove_const :Parser if const_defined? :Parser + const_set :Parser, parser + end + + # Return the constant located at _path_. The format of _path_ has to be + # either ::A::B::C or A::B::C. In any case A has to be located at the top + # level (absolute namespace path?). If there doesn't exist a constant at + # the given path, an ArgumentError is raised. + def deep_const_get(path) # :nodoc: + path = path.to_s + path.split(/::/).inject(Object) do |p, c| + case + when c.empty? then p + when p.const_defined?(c) then p.const_get(c) + else raise ArgumentError, "can't find const #{path}" + end + end + end + + # Set the module _generator_ to be used by JSON. + def generator=(generator) # :nodoc: + @generator = generator + generator_methods = generator::GeneratorMethods + for const in generator_methods.constants + klass = deep_const_get(const) + modul = generator_methods.const_get(const) + klass.class_eval do + instance_methods(false).each do |m| + m.to_s == 'to_json' and remove_method m + end + include modul + end + end + self.state = generator::State + const_set :State, self.state + end + + # Returns the JSON generator modul, that is used by JSON. This might be + # either JSON::Ext::Generator or JSON::Pure::Generator. + attr_reader :generator + + # Returns the JSON generator state class, that is used by JSON. This might + # be either JSON::Ext::Generator::State or JSON::Pure::Generator::State. + attr_accessor :state + + # This is create identifier, that is used to decide, if the _json_create_ + # hook of a class should be called. It defaults to 'json_class'. + attr_accessor :create_id + end + self.create_id = 'json_class' + + NaN = (-1.0) ** 0.5 + + Infinity = 1.0/0 + + MinusInfinity = -Infinity + + # The base exception for JSON errors. + class JSONError < StandardError; end + + # This exception is raised, if a parser error occurs. + class ParserError < JSONError; end + + # This exception is raised, if the nesting of parsed datastructures is too + # deep. + class NestingError < ParserError; end + + # This exception is raised, if a generator or unparser error occurs. + class GeneratorError < JSONError; end + # For backwards compatibility + UnparserError = GeneratorError + + # If a circular data structure is encountered while unparsing + # this exception is raised. + class CircularDatastructure < GeneratorError; end + + # This exception is raised, if the required unicode support is missing on the + # system. Usually this means, that the iconv library is not installed. + class MissingUnicodeSupport < JSONError; end + + module_function + + # Parse the JSON string _source_ into a Ruby data structure and return it. + # + # _opts_ can have the following + # keys: + # * *max_nesting*: The maximum depth of nesting allowed in the parsed data + # structures. Disable depth checking with :max_nesting => false, it defaults + # to 19. + # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in + # defiance of RFC 4627 to be parsed by the Parser. This option defaults + # to false. + # * *create_additions*: If set to false, the Parser doesn't create + # additions even if a matchin class and create_id was found. This option + # defaults to true. + def parse(source, opts = {}) + JSON.parser.new(source, opts).parse + end + + # Parse the JSON string _source_ into a Ruby data structure and return it. + # The bang version of the parse method, defaults to the more dangerous values + # for the _opts_ hash, so be sure only to parse trusted _source_ strings. + # + # _opts_ can have the following keys: + # * *max_nesting*: The maximum depth of nesting allowed in the parsed data + # structures. Enable depth checking with :max_nesting => anInteger. The parse! + # methods defaults to not doing max depth checking: This can be dangerous, + # if someone wants to fill up your stack. + # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in + # defiance of RFC 4627 to be parsed by the Parser. This option defaults + # to true. + # * *create_additions*: If set to false, the Parser doesn't create + # additions even if a matchin class and create_id was found. This option + # defaults to true. + def parse!(source, opts = {}) + opts = { + :max_nesting => false, + :allow_nan => true + }.update(opts) + JSON.parser.new(source, opts).parse + end + + # Unparse the Ruby data structure _obj_ into a single line JSON string and + # return it. _state_ is + # * a JSON::State object, + # * or a Hash like object (responding to to_hash), + # * an object convertible into a hash by a to_h method, + # that is used as or to configure a State object. + # + # It defaults to a state object, that creates the shortest possible JSON text + # in one line, checks for circular data structures and doesn't allow NaN, + # Infinity, and -Infinity. + # + # A _state_ hash can have the following keys: + # * *indent*: a string used to indent levels (default: ''), + # * *space*: a string that is put after, a : or , delimiter (default: ''), + # * *space_before*: a string that is put before a : pair delimiter (default: ''), + # * *object_nl*: a string that is put at the end of a JSON object (default: ''), + # * *array_nl*: a string that is put at the end of a JSON array (default: ''), + # * *check_circular*: true if checking for circular data structures + # should be done (the default), false otherwise. + # * *allow_nan*: true if NaN, Infinity, and -Infinity should be + # generated, otherwise an exception is thrown, if these values are + # encountered. This options defaults to false. + # * *max_nesting*: The maximum depth of nesting allowed in the data + # structures from which JSON is to be generated. Disable depth checking + # with :max_nesting => false, it defaults to 19. + # + # See also the fast_generate for the fastest creation method with the least + # amount of sanity checks, and the pretty_generate method for some + # defaults for a pretty output. + def generate(obj, state = nil) + if state + state = State.from_state(state) + else + state = State.new + end + obj.to_json(state) + end + + # :stopdoc: + # I want to deprecate these later, so I'll first be silent about them, and + # later delete them. + alias unparse generate + module_function :unparse + # :startdoc: + + # Unparse the Ruby data structure _obj_ into a single line JSON string and + # return it. This method disables the checks for circles in Ruby objects, and + # also generates NaN, Infinity, and, -Infinity float values. + # + # *WARNING*: Be careful not to pass any Ruby data structures with circles as + # _obj_ argument, because this will cause JSON to go into an infinite loop. + def fast_generate(obj) + obj.to_json(nil) + end + + # :stopdoc: + # I want to deprecate these later, so I'll first be silent about them, and later delete them. + alias fast_unparse fast_generate + module_function :fast_unparse + # :startdoc: + + # Unparse the Ruby data structure _obj_ into a JSON string and return it. The + # returned string is a prettier form of the string returned by #unparse. + # + # The _opts_ argument can be used to configure the generator, see the + # generate method for a more detailed explanation. + def pretty_generate(obj, opts = nil) + state = JSON.state.new( + :indent => ' ', + :space => ' ', + :object_nl => "\n", + :array_nl => "\n", + :check_circular => true + ) + if opts + if opts.respond_to? :to_hash + opts = opts.to_hash + elsif opts.respond_to? :to_h + opts = opts.to_h + else + raise TypeError, "can't convert #{opts.class} into Hash" + end + state.configure(opts) + end + obj.to_json(state) + end + + # :stopdoc: + # I want to deprecate these later, so I'll first be silent about them, and later delete them. + alias pretty_unparse pretty_generate + module_function :pretty_unparse + # :startdoc: + + # Load a ruby data structure from a JSON _source_ and return it. A source can + # either be a string-like object, an IO like object, or an object responding + # to the read method. If _proc_ was given, it will be called with any nested + # Ruby object as an argument recursively in depth first order. + # + # This method is part of the implementation of the load/dump interface of + # Marshal and YAML. + def load(source, proc = nil) + if source.respond_to? :to_str + source = source.to_str + elsif source.respond_to? :to_io + source = source.to_io.read + else + source = source.read + end + result = parse(source, :max_nesting => false, :allow_nan => true) + recurse_proc(result, &proc) if proc + result + end + + def recurse_proc(result, &proc) + case result + when Array + result.each { |x| recurse_proc x, &proc } + proc.call result + when Hash + result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc } + proc.call result + else + proc.call result + end + end + private :recurse_proc + module_function :recurse_proc + + alias restore load + module_function :restore + + # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns + # the result. + # + # If anIO (an IO like object or an object that responds to the write method) + # was given, the resulting JSON is written to it. + # + # If the number of nested arrays or objects exceeds _limit_ an ArgumentError + # exception is raised. This argument is similar (but not exactly the + # same!) to the _limit_ argument in Marshal.dump. + # + # This method is part of the implementation of the load/dump interface of + # Marshal and YAML. + def dump(obj, anIO = nil, limit = nil) + if anIO and limit.nil? + anIO = anIO.to_io if anIO.respond_to?(:to_io) + unless anIO.respond_to?(:write) + limit = anIO + anIO = nil + end + end + limit ||= 0 + result = generate(obj, :allow_nan => true, :max_nesting => limit) + if anIO + anIO.write result + anIO + else + result + end + rescue JSON::NestingError + raise ArgumentError, "exceed depth limit" + end +end + +module ::Kernel + private + + # Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in + # one line. + def j(*objs) + objs.each do |obj| + puts JSON::generate(obj, :allow_nan => true, :max_nesting => false) + end + nil + end + + # Ouputs _objs_ to STDOUT as JSON strings in a pretty format, with + # indentation and over many lines. + def jj(*objs) + objs.each do |obj| + puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false) + end + nil + end + + # If _object_ is string-like parse the string and return the parsed result as + # a Ruby data structure. Otherwise generate a JSON text from the Ruby data + # structure object and return it. + # + # The _opts_ argument is passed through to generate/parse respectively, see + # generate and parse for their documentation. + def JSON(object, opts = {}) + if object.respond_to? :to_str + JSON.parse(object.to_str, opts) + else + JSON.generate(object, opts) + end + end +end + +class ::Class + # Returns true, if this class can be used to create an instance + # from a serialised JSON string. The class has to implement a class + # method _json_create_ that expects a hash as first parameter, which includes + # the required data. + def json_creatable? + respond_to?(:json_create) + end +end diff --git a/lib/json/editor.rb b/lib/json/editor.rb new file mode 100644 index 0000000..1e13f33 --- /dev/null +++ b/lib/json/editor.rb @@ -0,0 +1,1371 @@ +# To use the GUI JSON editor, start the edit_json.rb executable script. It +# requires ruby-gtk to be installed. + +require 'gtk2' +require 'iconv' +require 'json' +require 'rbconfig' +require 'open-uri' + +module JSON + module Editor + include Gtk + + # Beginning of the editor window title + TITLE = 'JSON Editor'.freeze + + # Columns constants + ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2 + + # JSON primitive types (Containers) + CONTAINER_TYPES = %w[Array Hash].sort + # All JSON primitive types + ALL_TYPES = (%w[TrueClass FalseClass Numeric String NilClass] + + CONTAINER_TYPES).sort + + # The Nodes necessary for the tree representation of a JSON document + ALL_NODES = (ALL_TYPES + %w[Key]).sort + + DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event| + case event.keyval + when Gdk::Keyval::GDK_Return + dialog.response Dialog::RESPONSE_ACCEPT + when Gdk::Keyval::GDK_Escape + dialog.response Dialog::RESPONSE_REJECT + end + end + + # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache. + def Editor.fetch_icon(name) + @icon_cache ||= {} + unless @icon_cache.key?(name) + path = File.dirname(__FILE__) + @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm')) + end + @icon_cache[name] + end + + # Opens an error dialog on top of _window_ showing the error message + # _text_. + def Editor.error_dialog(window, text) + dialog = MessageDialog.new(window, Dialog::MODAL, + MessageDialog::ERROR, + MessageDialog::BUTTONS_CLOSE, text) + dialog.show_all + dialog.run + rescue TypeError + dialog = MessageDialog.new(Editor.window, Dialog::MODAL, + MessageDialog::ERROR, + MessageDialog::BUTTONS_CLOSE, text) + dialog.show_all + dialog.run + ensure + dialog.destroy if dialog + end + + # Opens a yes/no question dialog on top of _window_ showing the error + # message _text_. If yes was answered _true_ is returned, otherwise + # _false_. + def Editor.question_dialog(window, text) + dialog = MessageDialog.new(window, Dialog::MODAL, + MessageDialog::QUESTION, + MessageDialog::BUTTONS_YES_NO, text) + dialog.show_all + dialog.run do |response| + return Gtk::Dialog::RESPONSE_YES === response + end + ensure + dialog.destroy if dialog + end + + # Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby + # data structure and return it. + def Editor.model2data(iter) + return nil if iter.nil? + case iter.type + when 'Hash' + hash = {} + iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) } + hash + when 'Array' + array = Array.new(iter.n_children) + iter.each_with_index { |c, i| array[i] = Editor.model2data(c) } + array + when 'Key' + iter.content + when 'String' + iter.content + when 'Numeric' + content = iter.content + if /\./.match(content) + content.to_f + else + content.to_i + end + when 'TrueClass' + true + when 'FalseClass' + false + when 'NilClass' + nil + else + fail "Unknown type found in model: #{iter.type}" + end + end + + # Convert the Ruby data structure _data_ into tree model data for Gtk and + # returns the whole model. If the parameter _model_ wasn't given a new + # Gtk::TreeStore is created as the model. The _parent_ parameter specifies + # the parent node (iter, Gtk:TreeIter instance) to which the data is + # appended, alternativeley the result of the yielded block is used as iter. + def Editor.data2model(data, model = nil, parent = nil) + model ||= TreeStore.new(Gdk::Pixbuf, String, String) + iter = if block_given? + yield model + else + model.append(parent) + end + case data + when Hash + iter.type = 'Hash' + data.sort.each do |key, value| + pair_iter = model.append(iter) + pair_iter.type = 'Key' + pair_iter.content = key.to_s + Editor.data2model(value, model, pair_iter) + end + when Array + iter.type = 'Array' + data.each do |value| + Editor.data2model(value, model, iter) + end + when Numeric + iter.type = 'Numeric' + iter.content = data.to_s + when String, true, false, nil + iter.type = data.class.name + iter.content = data.nil? ? 'null' : data.to_s + else + iter.type = 'String' + iter.content = data.to_s + end + model + end + + # The Gtk::TreeIter class is reopened and some auxiliary methods are added. + class Gtk::TreeIter + include Enumerable + + # Traverse each of this Gtk::TreeIter instance's children + # and yield to them. + def each + n_children.times { |i| yield nth_child(i) } + end + + # Recursively traverse all nodes of this Gtk::TreeIter's subtree + # (including self) and yield to them. + def recursive_each(&block) + yield self + each do |i| + i.recursive_each(&block) + end + end + + # Remove the subtree of this Gtk::TreeIter instance from the + # model _model_. + def remove_subtree(model) + while current = first_child + model.remove(current) + end + end + + # Returns the type of this node. + def type + self[TYPE_COL] + end + + # Sets the type of this node to _value_. This implies setting + # the respective icon accordingly. + def type=(value) + self[TYPE_COL] = value + self[ICON_COL] = Editor.fetch_icon(value) + end + + # Returns the content of this node. + def content + self[CONTENT_COL] + end + + # Sets the content of this node to _value_. + def content=(value) + self[CONTENT_COL] = value + end + end + + # This module bundles some method, that can be used to create a menu. It + # should be included into the class in question. + module MenuExtension + include Gtk + + # Creates a Menu, that includes MenuExtension. _treeview_ is the + # Gtk::TreeView, on which it operates. + def initialize(treeview) + @treeview = treeview + @menu = Menu.new + end + + # Returns the Gtk::TreeView of this menu. + attr_reader :treeview + + # Returns the menu. + attr_reader :menu + + # Adds a Gtk::SeparatorMenuItem to this instance's #menu. + def add_separator + menu.append SeparatorMenuItem.new + end + + # Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label + # string, _klass_ is the item type, and _callback_ is the procedure, that + # is called if the _item_ is activated. + def add_item(label, keyval = nil, klass = MenuItem, &callback) + label = "#{label} (C-#{keyval.chr})" if keyval + item = klass.new(label) + item.signal_connect(:activate, &callback) + if keyval + self.signal_connect(:'key-press-event') do |item, event| + if event.state & Gdk::Window::ModifierType::CONTROL_MASK != 0 and + event.keyval == keyval + callback.call item + end + end + end + menu.append item + item + end + + # This method should be implemented in subclasses to create the #menu of + # this instance. It has to be called after an instance of this class is + # created, to build the menu. + def create + raise NotImplementedError + end + + def method_missing(*a, &b) + treeview.__send__(*a, &b) + end + end + + # This class creates the popup menu, that opens when clicking onto the + # treeview. + class PopUpMenu + include MenuExtension + + # Change the type or content of the selected node. + def change_node(item) + if current = selection.selected + parent = current.parent + old_type, old_content = current.type, current.content + if ALL_TYPES.include?(old_type) + @clipboard_data = Editor.model2data(current) + type, content = ask_for_element(parent, current.type, + current.content) + if type + current.type, current.content = type, content + current.remove_subtree(model) + toplevel.display_status("Changed a node in tree.") + window.change + end + else + toplevel.display_status( + "Cannot change node of type #{old_type} in tree!") + end + end + end + + # Cut the selected node and its subtree, and save it into the + # clipboard. + def cut_node(item) + if current = selection.selected + if current and current.type == 'Key' + @clipboard_data = { + current.content => Editor.model2data(current.first_child) + } + else + @clipboard_data = Editor.model2data(current) + end + model.remove(current) + window.change + toplevel.display_status("Cut a node from tree.") + end + end + + # Copy the selected node and its subtree, and save it into the + # clipboard. + def copy_node(item) + if current = selection.selected + if current and current.type == 'Key' + @clipboard_data = { + current.content => Editor.model2data(current.first_child) + } + else + @clipboard_data = Editor.model2data(current) + end + window.change + toplevel.display_status("Copied a node from tree.") + end + end + + # Paste the data in the clipboard into the selected Array or Hash by + # appending it. + def paste_node_appending(item) + if current = selection.selected + if @clipboard_data + case current.type + when 'Array' + Editor.data2model(@clipboard_data, model, current) + expand_collapse(current) + when 'Hash' + if @clipboard_data.is_a? Hash + parent = current.parent + hash = Editor.model2data(current) + model.remove(current) + hash.update(@clipboard_data) + Editor.data2model(hash, model, parent) + if parent + expand_collapse(parent) + elsif @expanded + expand_all + end + window.change + else + toplevel.display_status( + "Cannot paste non-#{current.type} data into '#{current.type}'!") + end + else + toplevel.display_status( + "Cannot paste node below '#{current.type}'!") + end + else + toplevel.display_status("Nothing to paste in clipboard!") + end + else + toplevel.display_status("Append a node into the root first!") + end + end + + # Paste the data in the clipboard into the selected Array inserting it + # before the selected element. + def paste_node_inserting_before(item) + if current = selection.selected + if @clipboard_data + parent = current.parent or return + parent_type = parent.type + if parent_type == 'Array' + selected_index = parent.each_with_index do |c, i| + break i if c == current + end + Editor.data2model(@clipboard_data, model, parent) do |m| + m.insert_before(parent, current) + end + expand_collapse(current) + toplevel.display_status("Inserted an element to " + + "'#{parent_type}' before index #{selected_index}.") + window.change + else + toplevel.display_status( + "Cannot insert node below '#{parent_type}'!") + end + else + toplevel.display_status("Nothing to paste in clipboard!") + end + else + toplevel.display_status("Append a node into the root first!") + end + end + + # Append a new node to the selected Hash or Array. + def append_new_node(item) + if parent = selection.selected + parent_type = parent.type + case parent_type + when 'Hash' + key, type, content = ask_for_hash_pair(parent) + key or return + iter = create_node(parent, 'Key', key) + iter = create_node(iter, type, content) + toplevel.display_status( + "Added a (key, value)-pair to '#{parent_type}'.") + window.change + when 'Array' + type, content = ask_for_element(parent) + type or return + iter = create_node(parent, type, content) + window.change + toplevel.display_status("Appendend an element to '#{parent_type}'.") + else + toplevel.display_status("Cannot append to '#{parent_type}'!") + end + else + type, content = ask_for_element + type or return + iter = create_node(nil, type, content) + window.change + end + end + + # Insert a new node into an Array before the selected element. + def insert_new_node(item) + if current = selection.selected + parent = current.parent or return + parent_parent = parent.parent + parent_type = parent.type + if parent_type == 'Array' + selected_index = parent.each_with_index do |c, i| + break i if c == current + end + type, content = ask_for_element(parent) + type or return + iter = model.insert_before(parent, current) + iter.type, iter.content = type, content + toplevel.display_status("Inserted an element to " + + "'#{parent_type}' before index #{selected_index}.") + window.change + else + toplevel.display_status( + "Cannot insert node below '#{parent_type}'!") + end + else + toplevel.display_status("Append a node into the root first!") + end + end + + # Recursively collapse/expand a subtree starting from the selected node. + def collapse_expand(item) + if current = selection.selected + if row_expanded?(current.path) + collapse_row(current.path) + else + expand_row(current.path, true) + end + else + toplevel.display_status("Append a node into the root first!") + end + end + + # Create the menu. + def create + add_item("Change node", ?n, &method(:change_node)) + add_separator + add_item("Cut node", ?X, &method(:cut_node)) + add_item("Copy node", ?C, &method(:copy_node)) + add_item("Paste node (appending)", ?A, &method(:paste_node_appending)) + add_item("Paste node (inserting before)", ?I, + &method(:paste_node_inserting_before)) + add_separator + add_item("Append new node", ?a, &method(:append_new_node)) + add_item("Insert new node before", ?i, &method(:insert_new_node)) + add_separator + add_item("Collapse/Expand node (recursively)", ?e, + &method(:collapse_expand)) + + menu.show_all + signal_connect(:button_press_event) do |widget, event| + if event.kind_of? Gdk::EventButton and event.button == 3 + menu.popup(nil, nil, event.button, event.time) + end + end + signal_connect(:popup_menu) do + menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME) + end + end + end + + # This class creates the File pulldown menu. + class FileMenu + include MenuExtension + + # Clear the model and filename, but ask to save the JSON document, if + # unsaved changes have occured. + def new(item) + window.clear + end + + # Open a file and load it into the editor. Ask to save the JSON document + # first, if unsaved changes have occured. + def open(item) + window.file_open + end + + def open_location(item) + window.location_open + end + + # Revert the current JSON document in the editor to the saved version. + def revert(item) + window.instance_eval do + @filename and file_open(@filename) + end + end + + # Save the current JSON document. + def save(item) + window.file_save + end + + # Save the current JSON document under the given filename. + def save_as(item) + window.file_save_as + end + + # Quit the editor, after asking to save any unsaved changes first. + def quit(item) + window.quit + end + + # Create the menu. + def create + title = MenuItem.new('File') + title.submenu = menu + add_item('New', &method(:new)) + add_item('Open', ?o, &method(:open)) + add_item('Open location', ?l, &method(:open_location)) + add_item('Revert', &method(:revert)) + add_separator + add_item('Save', ?s, &method(:save)) + add_item('Save As', ?S, &method(:save_as)) + add_separator + add_item('Quit', ?q, &method(:quit)) + title + end + end + + # This class creates the Edit pulldown menu. + class EditMenu + include MenuExtension + + # Copy data from model into primary clipboard. + def copy(item) + data = Editor.model2data(model.iter_first) + json = JSON.pretty_generate(data, :max_nesting => false) + c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY) + c.text = json + end + + # Copy json text from primary clipboard into model. + def paste(item) + c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY) + if json = c.wait_for_text + window.ask_save if @changed + begin + window.edit json + rescue JSON::ParserError + window.clear + end + end + end + + # Find a string in all nodes' contents and select the found node in the + # treeview. + def find(item) + @search = ask_for_find_term(@search) or return + iter = model.get_iter('0') or return + iter.recursive_each do |i| + if @iter + if @iter != i + next + else + @iter = nil + next + end + elsif @search.match(i[CONTENT_COL]) + set_cursor(i.path, nil, false) + @iter = i + break + end + end + end + + # Repeat the last search given by #find. + def find_again(item) + @search or return + iter = model.get_iter('0') + iter.recursive_each do |i| + if @iter + if @iter != i + next + else + @iter = nil + next + end + elsif @search.match(i[CONTENT_COL]) + set_cursor(i.path, nil, false) + @iter = i + break + end + end + end + + # Sort (Reverse sort) all elements of the selected array by the given + # expression. _x_ is the element in question. + def sort(item) + if current = selection.selected + if current.type == 'Array' + parent = current.parent + ary = Editor.model2data(current) + order, reverse = ask_for_order + order or return + begin + block = eval "lambda { |x| #{order} }" + if reverse + ary.sort! { |a,b| block[b] <=> block[a] } + else + ary.sort! { |a,b| block[a] <=> block[b] } + end + rescue => e + Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!") + else + Editor.data2model(ary, model, parent) do |m| + m.insert_before(parent, current) + end + model.remove(current) + expand_collapse(parent) + window.change + toplevel.display_status("Array has been sorted.") + end + else + toplevel.display_status("Only Array nodes can be sorted!") + end + else + toplevel.display_status("Select an Array to sort first!") + end + end + + # Create the menu. + def create + title = MenuItem.new('Edit') + title.submenu = menu + add_item('Copy', ?c, &method(:copy)) + add_item('Paste', ?v, &method(:paste)) + add_separator + add_item('Find', ?f, &method(:find)) + add_item('Find Again', ?g, &method(:find_again)) + add_separator + add_item('Sort', ?S, &method(:sort)) + title + end + end + + class OptionsMenu + include MenuExtension + + # Collapse/Expand all nodes by default. + def collapsed_nodes(item) + if expanded + self.expanded = false + collapse_all + else + self.expanded = true + expand_all + end + end + + # Toggle pretty saving mode on/off. + def pretty_saving(item) + @pretty_item.toggled + window.change + end + + attr_reader :pretty_item + + # Create the menu. + def create + title = MenuItem.new('Options') + title.submenu = menu + add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes)) + @pretty_item = add_item('Pretty saving', nil, CheckMenuItem, + &method(:pretty_saving)) + @pretty_item.active = true + window.unchange + title + end + end + + # This class inherits from Gtk::TreeView, to configure it and to add a lot + # of behaviour to it. + class JSONTreeView < Gtk::TreeView + include Gtk + + # Creates a JSONTreeView instance, the parameter _window_ is + # a MainWindow instance and used for self delegation. + def initialize(window) + @window = window + super(TreeStore.new(Gdk::Pixbuf, String, String)) + self.selection.mode = SELECTION_BROWSE + + @expanded = false + self.headers_visible = false + add_columns + add_popup_menu + end + + # Returns the MainWindow instance of this JSONTreeView. + attr_reader :window + + # Returns true, if nodes are autoexpanding, false otherwise. + attr_accessor :expanded + + private + + def add_columns + cell = CellRendererPixbuf.new + column = TreeViewColumn.new('Icon', cell, + 'pixbuf' => ICON_COL + ) + append_column(column) + + cell = CellRendererText.new + column = TreeViewColumn.new('Type', cell, + 'text' => TYPE_COL + ) + append_column(column) + + cell = CellRendererText.new + cell.editable = true + column = TreeViewColumn.new('Content', cell, + 'text' => CONTENT_COL + ) + cell.signal_connect(:edited, &method(:cell_edited)) + append_column(column) + end + + def unify_key(iter, key) + return unless iter.type == 'Key' + parent = iter.parent + if parent.any? { |c| c != iter and c.content == key } + old_key = key + i = 0 + begin + key = sprintf("%s.%d", old_key, i += 1) + end while parent.any? { |c| c != iter and c.content == key } + end + iter.content = key + end + + def cell_edited(cell, path, value) + iter = model.get_iter(path) + case iter.type + when 'Key' + unify_key(iter, value) + toplevel.display_status('Key has been changed.') + when 'FalseClass' + value.downcase! + if value == 'true' + iter.type, iter.content = 'TrueClass', 'true' + end + when 'TrueClass' + value.downcase! + if value == 'false' + iter.type, iter.content = 'FalseClass', 'false' + end + when 'Numeric' + iter.content = + if value == 'Infinity' + value + else + (Integer(value) rescue Float(value) rescue 0).to_s + end + when 'String' + iter.content = value + when 'Hash', 'Array' + return + else + fail "Unknown type found in model: #{iter.type}" + end + window.change + end + + def configure_value(value, type) + value.editable = false + case type + when 'Array', 'Hash' + value.text = '' + when 'TrueClass' + value.text = 'true' + when 'FalseClass' + value.text = 'false' + when 'NilClass' + value.text = 'null' + when 'Numeric', 'String' + value.text ||= '' + value.editable = true + else + raise ArgumentError, "unknown type '#{type}' encountered" + end + end + + def add_popup_menu + menu = PopUpMenu.new(self) + menu.create + end + + public + + # Create a _type_ node with content _content_, and add it to _parent_ + # in the model. If _parent_ is nil, create a new model and put it into + # the editor treeview. + def create_node(parent, type, content) + iter = if parent + model.append(parent) + else + new_model = Editor.data2model(nil) + toplevel.view_new_model(new_model) + new_model.iter_first + end + iter.type, iter.content = type, content + expand_collapse(parent) if parent + iter + end + + # Ask for a hash key, value pair to be added to the Hash node _parent_. + def ask_for_hash_pair(parent) + key_input = type_input = value_input = nil + + dialog = Dialog.new("New (key, value) pair for Hash", nil, nil, + [ Stock::OK, Dialog::RESPONSE_ACCEPT ], + [ Stock::CANCEL, Dialog::RESPONSE_REJECT ] + ) + dialog.width_request = 640 + + hbox = HBox.new(false, 5) + hbox.pack_start(Label.new("Key:"), false) + hbox.pack_start(key_input = Entry.new) + key_input.text = @key || '' + dialog.vbox.pack_start(hbox, false) + key_input.signal_connect(:activate) do + if parent.any? { |c| c.content == key_input.text } + toplevel.display_status('Key already exists in Hash!') + key_input.text = '' + else + toplevel.display_status('Key has been changed.') + end + end + + hbox = HBox.new(false, 5) + hbox.pack_start(Label.new("Type:"), false) + hbox.pack_start(type_input = ComboBox.new(true)) + ALL_TYPES.each { |t| type_input.append_text(t) } + type_input.active = @type || 0 + dialog.vbox.pack_start(hbox, false) + + type_input.signal_connect(:changed) do + value_input.editable = false + case ALL_TYPES[type_input.active] + when 'Array', 'Hash' + value_input.text = '' + when 'TrueClass' + value_input.text = 'true' + when 'FalseClass' + value_input.text = 'false' + when 'NilClass' + value_input.text = 'null' + else + value_input.text = '' + value_input.editable = true + end + end + + hbox = HBox.new(false, 5) + hbox.pack_start(Label.new("Value:"), false) + hbox.pack_start(value_input = Entry.new) + value_input.width_chars = 60 + value_input.text = @value || '' + dialog.vbox.pack_start(hbox, false) + + dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER) + dialog.show_all + self.focus = dialog + dialog.run do |response| + if response == Dialog::RESPONSE_ACCEPT + @key = key_input.text + type = ALL_TYPES[@type = type_input.active] + content = value_input.text + return @key, type, content + end + end + return + ensure + dialog.destroy + end + + # Ask for an element to be appended _parent_. + def ask_for_element(parent = nil, default_type = nil, value_text = @content) + type_input = value_input = nil + + dialog = Dialog.new( + "New element into #{parent ? parent.type : 'root'}", + nil, nil, + [ Stock::OK, Dialog::RESPONSE_ACCEPT ], + [ Stock::CANCEL, Dialog::RESPONSE_REJECT ] + ) + hbox = HBox.new(false, 5) + hbox.pack_start(Label.new("Type:"), false) + hbox.pack_start(type_input = ComboBox.new(true)) + default_active = 0 + types = parent ? ALL_TYPES : CONTAINER_TYPES + types.each_with_index do |t, i| + type_input.append_text(t) + if t == default_type + default_active = i + end + end + type_input.active = default_active + dialog.vbox.pack_start(hbox, false) + type_input.signal_connect(:changed) do + configure_value(value_input, types[type_input.active]) + end + + hbox = HBox.new(false, 5) + hbox.pack_start(Label.new("Value:"), false) + hbox.pack_start(value_input = Entry.new) + value_input.width_chars = 60 + value_input.text = value_text if value_text + configure_value(value_input, types[type_input.active]) + + dialog.vbox.pack_start(hbox, false) + + dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER) + dialog.show_all + self.focus = dialog + dialog.run do |response| + if response == Dialog::RESPONSE_ACCEPT + type = types[type_input.active] + @content = case type + when 'Numeric' + if (t = value_input.text) == 'Infinity' + 1 / 0.0 + else + Integer(t) rescue Float(t) rescue 0 + end + else + value_input.text + end.to_s + return type, @content + end + end + return + ensure + dialog.destroy if dialog + end + + # Ask for an order criteria for sorting, using _x_ for the element in + # question. Returns the order criterium, and true/false for reverse + # sorting. + def ask_for_order + dialog = Dialog.new( + "Give an order criterium for 'x'.", + nil, nil, + [ Stock::OK, Dialog::RESPONSE_ACCEPT ], + [ Stock::CANCEL, Dialog::RESPONSE_REJECT ] + ) + hbox = HBox.new(false, 5) + + hbox.pack_start(Label.new("Order:"), false) + hbox.pack_start(order_input = Entry.new) + order_input.text = @order || 'x' + order_input.width_chars = 60 + + hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'), false) + + dialog.vbox.pack_start(hbox, false) + + dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER) + dialog.show_all + self.focus = dialog + dialog.run do |response| + if response == Dialog::RESPONSE_ACCEPT + return @order = order_input.text, reverse_checkbox.active? + end + end + return + ensure + dialog.destroy if dialog + end + + # Ask for a find term to search for in the tree. Returns the term as a + # string. + def ask_for_find_term(search = nil) + dialog = Dialog.new( + "Find a node matching regex in tree.", + nil, nil, + [ Stock::OK, Dialog::RESPONSE_ACCEPT ], + [ Stock::CANCEL, Dialog::RESPONSE_REJECT ] + ) + hbox = HBox.new(false, 5) + + hbox.pack_start(Label.new("Regex:"), false) + hbox.pack_start(regex_input = Entry.new) + hbox.pack_start(icase_checkbox = CheckButton.new('Icase'), false) + regex_input.width_chars = 60 + if search + regex_input.text = search.source + icase_checkbox.active = search.casefold? + end + + dialog.vbox.pack_start(hbox, false) + + dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER) + dialog.show_all + self.focus = dialog + dialog.run do |response| + if response == Dialog::RESPONSE_ACCEPT + begin + return Regexp.new(regex_input.text, icase_checkbox.active? ? Regexp::IGNORECASE : 0) + rescue => e + Editor.error_dialog(self, "Evaluation of regex /#{regex_input.text}/ failed: #{e}!") + return + end + end + end + return + ensure + dialog.destroy if dialog + end + + # Expand or collapse row pointed to by _iter_ according + # to the #expanded attribute. + def expand_collapse(iter) + if expanded + expand_row(iter.path, true) + else + collapse_row(iter.path) + end + end + end + + # The editor main window + class MainWindow < Gtk::Window + include Gtk + + def initialize(encoding) + @changed = false + @encoding = encoding + super(TOPLEVEL) + display_title + set_default_size(800, 600) + signal_connect(:delete_event) { quit } + + vbox = VBox.new(false, 0) + add(vbox) + #vbox.border_width = 0 + + @treeview = JSONTreeView.new(self) + @treeview.signal_connect(:'cursor-changed') do + display_status('') + end + + menu_bar = create_menu_bar + vbox.pack_start(menu_bar, false, false, 0) + + sw = ScrolledWindow.new(nil, nil) + sw.shadow_type = SHADOW_ETCHED_IN + sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC) + vbox.pack_start(sw, true, true, 0) + sw.add(@treeview) + + @status_bar = Statusbar.new + vbox.pack_start(@status_bar, false, false, 0) + + @filename ||= nil + if @filename + data = read_data(@filename) + view_new_model Editor.data2model(data) + end + + signal_connect(:button_release_event) do |_,event| + if event.button == 2 + c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY) + if url = c.wait_for_text + location_open url + end + false + else + true + end + end + end + + # Creates the menu bar with the pulldown menus and returns it. + def create_menu_bar + menu_bar = MenuBar.new + @file_menu = FileMenu.new(@treeview) + menu_bar.append @file_menu.create + @edit_menu = EditMenu.new(@treeview) + menu_bar.append @edit_menu.create + @options_menu = OptionsMenu.new(@treeview) + menu_bar.append @options_menu.create + menu_bar + end + + # Sets editor status to changed, to indicate that the edited data + # containts unsaved changes. + def change + @changed = true + display_title + end + + # Sets editor status to unchanged, to indicate that the edited data + # doesn't containt unsaved changes. + def unchange + @changed = false + display_title + end + + # Puts a new model _model_ into the Gtk::TreeView to be edited. + def view_new_model(model) + @treeview.model = model + @treeview.expanded = true + @treeview.expand_all + unchange + end + + # Displays _text_ in the status bar. + def display_status(text) + @cid ||= nil + @status_bar.pop(@cid) if @cid + @cid = @status_bar.get_context_id('dummy') + @status_bar.push(@cid, text) + end + + # Opens a dialog, asking, if changes should be saved to a file. + def ask_save + if Editor.question_dialog(self, + "Unsaved changes to JSON model. Save?") + if @filename + file_save + else + file_save_as + end + end + end + + # Quit this editor, that is, leave this editor's main loop. + def quit + ask_save if @changed + if Gtk.main_level > 0 + destroy + Gtk.main_quit + end + nil + end + + # Display the new title according to the editor's current state. + def display_title + title = TITLE.dup + title << ": #@filename" if @filename + title << " *" if @changed + self.title = title + end + + # Clear the current model, after asking to save all unsaved changes. + def clear + ask_save if @changed + @filename = nil + self.view_new_model nil + end + + def check_pretty_printed(json) + pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1) + @options_menu.pretty_item.active = pretty + end + private :check_pretty_printed + + # Open the data at the location _uri_, if given. Otherwise open a dialog + # to ask for the _uri_. + def location_open(uri = nil) + uri = ask_for_location unless uri + uri or return + ask_save if @changed + data = load_location(uri) or return + view_new_model Editor.data2model(data) + end + + # Open the file _filename_ or call the #select_file method to ask for a + # filename. + def file_open(filename = nil) + filename = select_file('Open as a JSON file') unless filename + data = load_file(filename) or return + view_new_model Editor.data2model(data) + end + + # Edit the string _json_ in the editor. + def edit(json) + if json.respond_to? :read + json = json.read + end + data = parse_json json + view_new_model Editor.data2model(data) + end + + # Save the current file. + def file_save + if @filename + store_file(@filename) + else + file_save_as + end + end + + # Save the current file as the filename + def file_save_as + filename = select_file('Save as a JSON file') + store_file(filename) + end + + # Store the current JSON document to _path_. + def store_file(path) + if path + data = Editor.model2data(@treeview.model.iter_first) + File.open(path + '.tmp', 'wb') do |output| + data or break + if @options_menu.pretty_item.active? + output.puts JSON.pretty_generate(data, :max_nesting => false) + else + output.write JSON.generate(data, :max_nesting => false) + end + end + File.rename path + '.tmp', path + @filename = path + toplevel.display_status("Saved data to '#@filename'.") + unchange + end + rescue SystemCallError => e + Editor.error_dialog(self, "Failed to store JSON file: #{e}!") + end + + # Load the file named _filename_ into the editor as a JSON document. + def load_file(filename) + if filename + if File.directory?(filename) + Editor.error_dialog(self, "Try to select a JSON file!") + nil + else + @filename = filename + if data = read_data(filename) + toplevel.display_status("Loaded data from '#@filename'.") + end + display_title + data + end + end + end + + # Load the data at location _uri_ into the editor as a JSON document. + def load_location(uri) + data = read_data(uri) or return + @filename = nil + toplevel.display_status("Loaded data from '#{uri}'.") + display_title + data + end + + def parse_json(json) + check_pretty_printed(json) + if @encoding && !/^utf8$/i.match(@encoding) + iconverter = Iconv.new('utf8', @encoding) + json = iconverter.iconv(json) + end + JSON::parse(json, :max_nesting => false, :create_additions => false) + end + private :parse_json + + # Read a JSON document from the file named _filename_, parse it into a + # ruby data structure, and return the data. + def read_data(filename) + open(filename) do |f| + json = f.read + return parse_json(json) + end + rescue => e + Editor.error_dialog(self, "Failed to parse JSON file: #{e}!") + return + end + + # Open a file selecton dialog, displaying _message_, and return the + # selected filename or nil, if no file was selected. + def select_file(message) + filename = nil + fs = FileSelection.new(message) + fs.set_modal(true) + @default_dir = File.join(Dir.pwd, '') unless @default_dir + fs.set_filename(@default_dir) + fs.set_transient_for(self) + fs.signal_connect(:destroy) { Gtk.main_quit } + fs.ok_button.signal_connect(:clicked) do + filename = fs.filename + @default_dir = File.join(File.dirname(filename), '') + fs.destroy + Gtk.main_quit + end + fs.cancel_button.signal_connect(:clicked) do + fs.destroy + Gtk.main_quit + end + fs.show_all + Gtk.main + filename + end + + # Ask for location URI a to load data from. Returns the URI as a string. + def ask_for_location + dialog = Dialog.new( + "Load data from location...", + nil, nil, + [ Stock::OK, Dialog::RESPONSE_ACCEPT ], + [ Stock::CANCEL, Dialog::RESPONSE_REJECT ] + ) + hbox = HBox.new(false, 5) + + hbox.pack_start(Label.new("Location:"), false) + hbox.pack_start(location_input = Entry.new) + location_input.width_chars = 60 + location_input.text = @location || '' + + dialog.vbox.pack_start(hbox, false) + + dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER) + dialog.show_all + dialog.run do |response| + if response == Dialog::RESPONSE_ACCEPT + return @location = location_input.text + end + end + return + ensure + dialog.destroy if dialog + end + end + + class << self + # Starts a JSON Editor. If a block was given, it yields + # to the JSON::Editor::MainWindow instance. + def start(encoding = 'utf8') # :yield: window + Gtk.init + @window = Editor::MainWindow.new(encoding) + @window.icon_list = [ Editor.fetch_icon('json') ] + yield @window if block_given? + @window.show_all + Gtk.main + end + + # Edit the string _json_ with encoding _encoding_ in the editor. + def edit(json, encoding = 'utf8') + start(encoding) do |window| + window.edit json + end + end + + attr_reader :window + end + end +end diff --git a/lib/json/ext.rb b/lib/json/ext.rb new file mode 100644 index 0000000..719e560 --- /dev/null +++ b/lib/json/ext.rb @@ -0,0 +1,15 @@ +require 'json/common' + +module JSON + # This module holds all the modules/classes that implement JSON's + # functionality as C extensions. + module Ext + require 'json/ext/parser' + require 'json/ext/generator' + $DEBUG and warn "Using c extension for JSON." + JSON.parser = Parser + JSON.generator = Generator + end + + JSON_LOADED = true +end diff --git a/lib/json/ext/.keep b/lib/json/ext/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/json/ext/.keep diff --git a/lib/json/json.xpm b/lib/json/json.xpm new file mode 100644 index 0000000..2cb626b --- /dev/null +++ b/lib/json/json.xpm @@ -0,0 +1,1499 @@ +/* XPM */ +static char * json_xpm[] = { +"64 64 1432 2", +" c None", +". c #641839", +"+ c #CF163C", +"@ c #D31C3B", +"# c #E11A38", +"$ c #5F242D", +"% c #320C22", +"& c #9B532D", +"* c #F32E34", +"= c #820F33", +"- c #4B0F34", +"; c #8E1237", +"> c #944029", +", c #961325", +"' c #A00C24", +") c #872C23", +"! c #694021", +"~ c #590D1F", +"{ c #420528", +"] c #D85A2D", +"^ c #7E092B", +"/ c #0E0925", +"( c #0D081F", +"_ c #0F081E", +": c #12071F", +"< c #360620", +"[ c #682A21", +"} c #673F21", +"| c #780E21", +"1 c #A82320", +"2 c #8D1D1F", +"3 c #970127", +"4 c #0D0123", +"5 c #0D0324", +"6 c #3B1E28", +"7 c #C28429", +"8 c #0C0523", +"9 c #0C041E", +"0 c #0E031A", +"a c #11031A", +"b c #13031B", +"c c #13031C", +"d c #11031D", +"e c #19051E", +"f c #390E20", +"g c #9C0C20", +"h c #C00721", +"i c #980320", +"j c #14031E", +"k c #CD9F32", +"l c #C29F2E", +"m c #0F0325", +"n c #0D0321", +"o c #0E0324", +"p c #D08329", +"q c #9D1B27", +"r c #1C0320", +"s c #0D011A", +"t c #120117", +"u c #130017", +"v c #150018", +"w c #160119", +"x c #17021A", +"y c #15021B", +"z c #11021E", +"A c #0F021F", +"B c #8C1821", +"C c #CF4522", +"D c #831821", +"E c #BA7033", +"F c #EDB339", +"G c #C89733", +"H c #280727", +"I c #0F051F", +"J c #0E0420", +"K c #591F27", +"L c #E47129", +"M c #612224", +"N c #0C021D", +"O c #120018", +"P c #140017", +"Q c #170017", +"R c #190018", +"S c #1B0019", +"T c #1B011A", +"U c #18011B", +"V c #15011C", +"W c #12031E", +"X c #460A21", +"Y c #A13823", +"Z c #784323", +"` c #5A0C21", +" . c #BC4530", +".. c #EB5B38", +"+. c #CE4E3B", +"@. c #DD9334", +"#. c #751A27", +"$. c #11071E", +"%. c #0F041C", +"&. c #1E0824", +"*. c #955A28", +"=. c #9A5027", +"-. c #1E0321", +";. c #11011A", +">. c #140018", +",. c #180018", +"'. c #1F001A", +"). c #20001B", +"!. c #1E001A", +"~. c #1B001A", +"{. c #16021B", +"]. c #16041E", +"^. c #220622", +"/. c #5F3525", +"(. c #DE5724", +"_. c #611021", +":. c #0F0925", +"<. c #D1892E", +"[. c #F27036", +"}. c #EC633B", +"|. c #DA293C", +"1. c #E64833", +"2. c #912226", +"3. c #11081C", +"4. c #110419", +"5. c #0F041E", +"6. c #451425", +"7. c #BF6F28", +"8. c #332225", +"9. c #0E021E", +"0. c #13001B", +"a. c #17001A", +"b. c #1C001B", +"c. c #21001C", +"d. c #23001C", +"e. c #21001B", +"f. c #19021A", +"g. c #17041E", +"h. c #150721", +"i. c #602424", +"j. c #D51223", +"k. c #540820", +"l. c #D04D2D", +"m. c #EA8933", +"n. c #875637", +"o. c #88543A", +"p. c #E5923A", +"q. c #891931", +"r. c #130B25", +"s. c #10051B", +"t. c #110217", +"u. c #12021A", +"v. c #761826", +"w. c #E2A728", +"x. c #300224", +"y. c #10011E", +"z. c #16001B", +"A. c #1B001B", +"B. c #21001A", +"C. c #1E0019", +"D. c #1D0019", +"E. c #1A011A", +"F. c #17031C", +"G. c #120720", +"H. c #4E0822", +"I. c #670721", +"J. c #C07630", +"K. c #F59734", +"L. c #BE1B35", +"M. c #0E1435", +"N. c #522037", +"O. c #DB8039", +"P. c #D45933", +"Q. c #420927", +"R. c #0F041D", +"S. c #140118", +"T. c #13021D", +"U. c #100423", +"V. c #7B6227", +"W. c #C04326", +"X. c #0E0020", +"Y. c #13001D", +"Z. c #18001B", +"`. c #1E001B", +" + c #22001C", +".+ c #22001B", +"++ c #1B011B", +"@+ c #16041D", +"#+ c #130520", +"$+ c #860521", +"%+ c #710520", +"&+ c #670A2A", +"*+ c #A66431", +"=+ c #E97536", +"-+ c #F8833A", +";+ c #F77A3A", +">+ c #C45337", +",+ c #0A1C35", +"'+ c #993638", +")+ c #F7863B", +"!+ c #F49736", +"~+ c #94462B", +"{+ c #0E031F", +"]+ c #130119", +"^+ c #160018", +"/+ c #16011B", +"(+ c #15021F", +"_+ c #120123", +":+ c #A65C28", +"<+ c #5C4D23", +"[+ c #0F001F", +"}+ c #14001D", +"|+ c #1A001B", +"1+ c #1F001B", +"2+ c #24001D", +"3+ c #25001D", +"4+ c #24001C", +"5+ c #1F001C", +"6+ c #1A011C", +"7+ c #16021E", +"8+ c #3F0421", +"9+ c #BC0522", +"0+ c #1C041E", +"a+ c #7F5531", +"b+ c #E68A38", +"c+ c #F8933E", +"d+ c #FA7942", +"e+ c #FB7543", +"f+ c #FA6F41", +"g+ c #F1793D", +"h+ c #7D3B3A", +"i+ c #28263B", +"j+ c #D45441", +"k+ c #F8A238", +"l+ c #996B2D", +"m+ c #0E0421", +"n+ c #12011A", +"o+ c #180019", +"p+ c #17001C", +"q+ c #12001F", +"r+ c #4C2B2A", +"s+ c #DB8130", +"t+ c #540023", +"u+ c #0F0120", +"v+ c #16011C", +"w+ c #22001D", +"x+ c #25001F", +"y+ c #26001F", +"z+ c #25001E", +"A+ c #24001E", +"B+ c #1D001C", +"C+ c #18011D", +"D+ c #16031F", +"E+ c #3C0522", +"F+ c #9B0821", +"G+ c #13041E", +"H+ c #F6462E", +"I+ c #E6AB37", +"J+ c #E7A03E", +"K+ c #FA9F44", +"L+ c #FB8A48", +"M+ c #FD7A4A", +"N+ c #FD794A", +"O+ c #FD7748", +"P+ c #FD7E45", +"Q+ c #FD8343", +"R+ c #FB5D42", +"S+ c #6E3A40", +"T+ c #EE8A37", +"U+ c #7E252B", +"V+ c #100520", +"W+ c #13011A", +"X+ c #170019", +"Y+ c #15001C", +"Z+ c #0F0020", +"`+ c #564427", +" @ c #E0BA29", +".@ c #5E2B25", +"+@ c #10011F", +"@@ c #17011C", +"#@ c #1E001D", +"$@ c #23001F", +"%@ c #250020", +"&@ c #24001F", +"*@ c #23001E", +"=@ c #21001E", +"-@ c #1B001C", +";@ c #17021D", +">@ c #14041E", +",@ c #AC0B25", +"'@ c #5E1420", +")@ c #F28635", +"!@ c #C2733E", +"~@ c #984C44", +"{@ c #EA9148", +"]@ c #FB844B", +"^@ c #FD7E4C", +"/@ c #FE7E4C", +"(@ c #FE7E4B", +"_@ c #FE7749", +":@ c #FD7148", +"<@ c #FB7D46", +"[@ c #F89641", +"}@ c #B95634", +"|@ c #0D0927", +"1@ c #11041D", +"2@ c #150119", +"3@ c #180017", +"4@ c #16001A", +"5@ c #13001E", +"6@ c #110023", +"7@ c #944C29", +"8@ c #EE6229", +"9@ c #3D0324", +"0@ c #12021F", +"a@ c #19011D", +"b@ c #21001F", +"c@ c #22001F", +"d@ c #20001E", +"e@ c #1F001D", +"f@ c #1C001C", +"g@ c #19011C", +"h@ c #3D1621", +"i@ c #B53622", +"j@ c #31061F", +"k@ c #841D34", +"l@ c #F2703F", +"m@ c #C14445", +"n@ c #E67349", +"o@ c #FB8E4B", +"p@ c #FD834C", +"q@ c #FE834D", +"r@ c #FE834C", +"s@ c #FE804C", +"t@ c #FD814B", +"u@ c #FB7D49", +"v@ c #F79B43", +"w@ c #AF1234", +"x@ c #0D0625", +"y@ c #13021C", +"z@ c #1A0019", +"A@ c #190019", +"B@ c #410225", +"C@ c #D39729", +"D@ c #AA5927", +"E@ c #0E0422", +"F@ c #15021E", +"G@ c #1A011D", +"H@ c #1D001D", +"I@ c #15031D", +"J@ c #240820", +"K@ c #A01023", +"L@ c #670B21", +"M@ c #3D0D33", +"N@ c #E63C3E", +"O@ c #EF7C45", +"P@ c #F59048", +"Q@ c #FB944A", +"R@ c #FD904A", +"S@ c #FE8E4B", +"T@ c #FE854A", +"U@ c #FE854B", +"V@ c #FE884C", +"W@ c #FC954B", +"X@ c #F8AB45", +"Y@ c #C37A35", +"Z@ c #0D0425", +"`@ c #13011B", +" # c #170018", +".# c #1A0018", +"+# c #1C0019", +"@# c #15001B", +"## c #100120", +"$# c #311F25", +"%# c #E68E28", +"&# c #7A1425", +"*# c #130321", +"=# c #17011E", +"-# c #1A001D", +";# c #19001B", +"># c #16021C", +",# c #130521", +"'# c #6F3123", +")# c #6D3022", +"!# c #C89433", +"~# c #EA7E3E", +"{# c #DB2943", +"]# c #EF7745", +"^# c #FB8544", +"/# c #FD9A43", +"(# c #FE9941", +"_# c #FE9D43", +":# c #FEA548", +"<# c #FEAE49", +"[# c #FCB944", +"}# c #CA9F35", +"|# c #0E0225", +"1# c #11001B", +"2# c #160019", +"3# c #12011B", +"4# c #0F0220", +"5# c #351D26", +"6# c #D85B28", +"7# c #6C0F26", +"8# c #190121", +"9# c #1B001E", +"0# c #1A001C", +"a# c #1D001B", +"b# c #130220", +"c# c #703A23", +"d# c #713A23", +"e# c #140327", +"f# c #411B36", +"g# c #C8713E", +"h# c #7A3A3F", +"i# c #CE2C3C", +"j# c #E77338", +"k# c #9C6535", +"l# c #9C6233", +"m# c #9C6332", +"n# c #9C6A35", +"o# c #C37D3C", +"p# c #FEAC41", +"q# c #FEC23E", +"r# c #826330", +"s# c #100122", +"t# c #120019", +"u# c #150017", +"v# c #190017", +"w# c #1B0018", +"x# c #12001A", +"y# c #10021F", +"z# c #1A0326", +"A# c #5F292A", +"B# c #7B4E29", +"C# c #3C0E25", +"D# c #1A0020", +"E# c #14021F", +"F# c #723B23", +"G# c #14001A", +"H# c #58042A", +"I# c #A28337", +"J# c #C8813B", +"K# c #B14B38", +"L# c #761231", +"M# c #5A132A", +"N# c #0D0726", +"O# c #0C0623", +"P# c #0B0723", +"Q# c #0B0A26", +"R# c #321C2D", +"S# c #C45B33", +"T# c #FEBB33", +"U# c #13052A", +"V# c #13011F", +"W# c #160017", +"X# c #15001A", +"Y# c #12001D", +"Z# c #94062A", +"`# c #630D2C", +" $ c #85292B", +".$ c #AA5E29", +"+$ c #1F0123", +"@$ c #19011F", +"#$ c #1E001C", +"$$ c #15031F", +"%$ c #712122", +"&$ c #712223", +"*$ c #14011B", +"=$ c #110321", +"-$ c #AF0C2B", +";$ c #E7D534", +">$ c #EAC934", +",$ c #84582D", +"'$ c #1B0824", +")$ c #11041E", +"!$ c #10021B", +"~$ c #100119", +"{$ c #100218", +"]$ c #0F041A", +"^$ c #0E0720", +"/$ c #2C1026", +"($ c #D8A328", +"_$ c #140322", +":$ c #160016", +"<$ c #14001F", +"[$ c #120024", +"}$ c #100128", +"|$ c #3C032F", +"1$ c #2C062E", +"2$ c #29022B", +"3$ c #A31D29", +"4$ c #976A25", +"5$ c #1A0321", +"6$ c #17031E", +"7$ c #1B021D", +"8$ c #20001C", +"9$ c #14041F", +"0$ c #703422", +"a$ c #6F3522", +"b$ c #8D0328", +"c$ c #920329", +"d$ c #0F0326", +"e$ c #100321", +"f$ c #11021B", +"g$ c #130117", +"h$ c #140016", +"i$ c #150015", +"j$ c #140015", +"k$ c #130116", +"l$ c #120219", +"m$ c #11031C", +"n$ c #12031D", +"o$ c #170016", +"p$ c #160020", +"q$ c #250029", +"r$ c #670033", +"s$ c #DCA238", +"t$ c #F5C736", +"u$ c #9A732E", +"v$ c #110227", +"w$ c #110324", +"x$ c #811924", +"y$ c #A04323", +"z$ c #250721", +"A$ c #1A041F", +"B$ c #1E011D", +"C$ c #1C011C", +"D$ c #18031D", +"E$ c #130721", +"F$ c #6F3623", +"G$ c #6B3622", +"H$ c #1A001A", +"I$ c #14011F", +"J$ c #12011E", +"K$ c #11011C", +"L$ c #140117", +"M$ c #170015", +"N$ c #150016", +"O$ c #120119", +"P$ c #11011B", +"Q$ c #11001A", +"R$ c #130018", +"S$ c #170118", +"T$ c #170119", +"U$ c #18021E", +"V$ c #1A0126", +"W$ c #6F2332", +"X$ c #E5563B", +"Y$ c #F1B83F", +"Z$ c #F6CC38", +"`$ c #9D7A2D", +" % c #130123", +".% c #130320", +"+% c #2A0721", +"@% c #B00E24", +"#% c #7D0B23", +"$% c #1F0522", +"%% c #1E0220", +"&% c #1D011E", +"*% c #1A031E", +"=% c #15051F", +"-% c #241322", +";% c #A32F23", +">% c #670E21", +",% c #1C001A", +"'% c #19001A", +")% c #180016", +"!% c #160118", +"~% c #140219", +"{% c #11021C", +"]% c #10021E", +"^% c #0F011D", +"/% c #170117", +"(% c #160219", +"_% c #17041D", +":% c #190523", +"<% c #8C042E", +"[% c #B65838", +"}% c #E9D73F", +"|% c #EED43E", +"1% c #D85538", +"2% c #493129", +"3% c #130120", +"4% c #15021D", +"5% c #330822", +"6% c #8A0825", +"7% c #3C0424", +"8% c #1E0322", +"9% c #1C0321", +"0% c #180421", +"a% c #130822", +"b% c #AF2D24", +"c% c #BC5623", +"d% c #2F071F", +"e% c #1A041C", +"f% c #1C031C", +"g% c #1D011C", +"h% c #160117", +"i% c #150419", +"j% c #12081D", +"k% c #0F0923", +"l% c #A77027", +"m% c #A60525", +"n% c #11021A", +"o% c #130218", +"p% c #150319", +"q% c #16061D", +"r% c #180923", +"s% c #9C1D2B", +"t% c #A32636", +"u% c #A66E3B", +"v% c #4B2E3C", +"w% c #412C36", +"x% c #36012D", +"y% c #140123", +"z% c #17001E", +"A% c #19011B", +"B% c #1A0421", +"C% c #340425", +"D% c #9E0326", +"E% c #1F0424", +"F% c #1C0524", +"G% c #180724", +"H% c #A91024", +"I% c #D55D24", +"J% c #90071E", +"K% c #3C051D", +"L% c #1C021C", +"M% c #1C011A", +"N% c #1D001A", +"O% c #160116", +"P% c #150216", +"Q% c #140217", +"R% c #140618", +"S% c #120D1D", +"T% c #231925", +"U% c #B16A2E", +"V% c #FDAC34", +"W% c #D58631", +"X% c #280E2A", +"Y% c #0D0A23", +"Z% c #0F0920", +"`% c #120C21", +" & c #1F1026", +".& c #A3352E", +"+& c #EE9F36", +"@& c #5D2A3C", +"#& c #960D3C", +"$& c #970638", +"%& c #A00330", +"&& c #4D0126", +"*& c #1C001F", +"=& c #280120", +"-& c #290223", +";& c #1F0425", +">& c #260726", +",& c #340A26", +"'& c #850925", +")& c #3A0823", +"!& c #82071D", +"~& c #5E071D", +"{& c #18051C", +"]& c #18021A", +"^& c #190118", +"/& c #160217", +"(& c #150418", +"_& c #130618", +":& c #110718", +"<& c #10081A", +"[& c #110D1D", +"}& c #291C24", +"|& c #A73B2D", +"1& c #FD6B36", +"2& c #FD853C", +"3& c #FD863B", +"4& c #C24A35", +"5& c #6B442F", +"6& c #6D302D", +"7& c #6E252E", +"8& c #8E3B32", +"9& c #DE7739", +"0& c #F48E3F", +"a& c #DD8D41", +"b& c #854F3D", +"c& c #7E2D35", +"d& c #33082B", +"e& c #1C0222", +"f& c #20001F", +"g& c #1F0222", +"h& c #1A0524", +"i& c #440C27", +"j& c #BC1427", +"k& c #20041B", +"l& c #53061C", +"m& c #25071B", +"n& c #11061A", +"o& c #130418", +"p& c #140317", +"q& c #150217", +"r& c #160318", +"s& c #12051B", +"t& c #100C1D", +"u& c #0E101E", +"v& c #0C121F", +"w& c #0C1321", +"x& c #781725", +"y& c #B25D2C", +"z& c #FA6335", +"A& c #FD633C", +"B& c #FE6D42", +"C& c #FE7C42", +"D& c #FE813F", +"E& c #FE873C", +"F& c #FD743B", +"G& c #FB683B", +"H& c #FA7A3E", +"I& c #F98242", +"J& c #F97844", +"K& c #F98943", +"L& c #F79C3D", +"M& c #A25133", +"N& c #280B28", +"O& c #1D021F", +"P& c #1F011C", +"Q& c #280321", +"R& c #1C0724", +"S& c #3F1C27", +"T& c #D33C27", +"U& c #0E061B", +"V& c #0C091C", +"W& c #0C0A1B", +"X& c #0E091A", +"Y& c #11081B", +"Z& c #100A20", +"`& c #0E0D23", +" * c #551227", +".* c #B21829", +"+* c #C42329", +"@* c #C62C29", +"#* c #C55429", +"$* c #E76F2B", +"%* c #F14232", +"&* c #F95E3A", +"** c #FC6740", +"=* c #FE6E45", +"-* c #FE7246", +";* c #FE7545", +">* c #FE7744", +",* c #FD7745", +"'* c #FD7845", +")* c #FD7847", +"!* c #FD7948", +"~* c #FD7B44", +"{* c #FC7C3B", +"]* c #6F3130", +"^* c #140B24", +"/* c #19031D", +"(* c #1C011B", +"_* c #5A011F", +":* c #B70421", +"<* c #380824", +"[* c #3E2626", +"}* c #9F5626", +"|* c #13051E", +"1* c #360A21", +"2* c #361223", +"3* c #371724", +"4* c #381824", +"5* c #3B1524", +"6* c #3E1E26", +"7* c #471A29", +"8* c #DB252E", +"9* c #ED2733", +"0* c #EE5436", +"a* c #F04237", +"b* c #F33934", +"c* c #F53D2F", +"d* c #D7312B", +"e* c #AF212B", +"f* c #3A2C31", +"g* c #F65F39", +"h* c #FB6F41", +"i* c #FD6D45", +"j* c #FE7047", +"k* c #FE7647", +"l* c #FE7847", +"m* c #FE7848", +"n* c #FE7748", +"o* c #FE7948", +"p* c #FE7C48", +"q* c #FE7C47", +"r* c #FE7642", +"s* c #FE7439", +"t* c #6D332C", +"u* c #100B21", +"v* c #16031B", +"w* c #2B001B", +"x* c #22011F", +"y* c #220521", +"z* c #1B0A23", +"A* c #421425", +"B* c #951924", +"C* c #381023", +"D* c #E94028", +"E* c #E7302B", +"F* c #EF432D", +"G* c #F4302E", +"H* c #F32C30", +"I* c #CB4432", +"J* c #DD3235", +"K* c #EF4B3A", +"L* c #F0333E", +"M* c #CC3D3F", +"N* c #E4313C", +"O* c #F34834", +"P* c #D13E2C", +"Q* c #431825", +"R* c #0E1424", +"S* c #3C202C", +"T* c #F15537", +"U* c #F97140", +"V* c #FC6E45", +"W* c #FE7547", +"X* c #FE7947", +"Y* c #FE7B48", +"Z* c #FE7D48", +"`* c #FE8047", +" = c #FE7A42", +".= c #FE7A38", +"+= c #6D442B", +"@= c #0F0B21", +"#= c #15031A", +"$= c #49001B", +"%= c #2F001C", +"&= c #21021E", +"*= c #220620", +"== c #1B0D23", +"-= c #641625", +";= c #951823", +">= c #390F25", +",= c #AC3A2A", +"'= c #B6492E", +")= c #ED7531", +"!= c #F45A34", +"~= c #F54C36", +"{= c #C72D39", +"]= c #DE283C", +"^= c #F33B40", +"/= c #F34142", +"(= c #D0393F", +"_= c #E72E39", +":= c #DB3C2E", +"<= c #461724", +"[= c #0F0D1E", +"}= c #140B1E", +"|= c #341427", +"1= c #CB4834", +"2= c #F7743F", +"3= c #FB7145", +"4= c #FE7747", +"5= c #FE7A47", +"6= c #FF7B48", +"7= c #FF7C48", +"8= c #FE7F47", +"9= c #FE8247", +"0= c #FE8642", +"a= c #FE8439", +"b= c #6D442D", +"c= c #0F0A21", +"d= c #14031A", +"e= c #20031D", +"f= c #210821", +"g= c #191024", +"h= c #CC1C25", +"i= c #961423", +"j= c #2C162C", +"k= c #BD242E", +"l= c #EF2C31", +"m= c #F54C34", +"n= c #F34037", +"o= c #F5353A", +"p= c #F7413D", +"q= c #F8423D", +"r= c #F93A39", +"s= c #F95731", +"t= c #341425", +"u= c #110A1D", +"v= c #140619", +"w= c #18051B", +"x= c #200F26", +"y= c #864833", +"z= c #F8773F", +"A= c #FC7445", +"B= c #FF7E48", +"C= c #FF7E49", +"D= c #FF7D49", +"E= c #FF7D48", +"F= c #FE8347", +"G= c #FE8743", +"H= c #FE893B", +"I= c #6E452F", +"J= c #100E23", +"K= c #14041A", +"L= c #55041D", +"M= c #540921", +"N= c #161124", +"O= c #CE6A25", +"P= c #3F1129", +"Q= c #170A29", +"R= c #0F0F29", +"S= c #15132B", +"T= c #1E182D", +"U= c #A82B3D", +"V= c #CB6633", +"W= c #CC6932", +"X= c #CC3D2D", +"Y= c #331225", +"Z= c #0F091C", +"`= c #120417", +" - c #160216", +".- c #190419", +"+- c #210F26", +"@- c #8C4934", +"#- c #F97A40", +"$- c #FC7545", +"%- c #FF7B49", +"&- c #FE7D46", +"*- c #FE7E43", +"=- c #FD7B3E", +"-- c #FA6934", +";- c #532328", +">- c #130B1D", +",- c #150519", +"'- c #14041C", +")- c #120920", +"!- c #C43624", +"~- c #A21E23", +"{- c #F87C30", +"]- c #C9302D", +"^- c #300F2A", +"/- c #591129", +"(- c #171328", +"_- c #171628", +":- c #141829", +"<- c #101A2B", +"[- c #0F172B", +"}- c #0F1226", +"|- c #0E0C20", +"1- c #100619", +"2- c #140316", +"3- c #19051B", +"4- c #3C1428", +"5- c #E04B36", +"6- c #FA7B41", +"7- c #FD7346", +"8- c #FE7548", +"9- c #FF7849", +"0- c #FF7749", +"a- c #FE7B47", +"b- c #FE7945", +"c- c #FC7740", +"d- c #FA7E39", +"e- c #C1432F", +"f- c #131523", +"g- c #130A1C", +"h- c #420621", +"i- c #D08423", +"j- c #F87739", +"k- c #C03D37", +"l- c #962B34", +"m- c #A14332", +"n- c #E54B30", +"o- c #9E3E2F", +"p- c #7F262E", +"q- c #922D2E", +"r- c #9C4B2E", +"s- c #65212C", +"t- c #101628", +"u- c #101022", +"v- c #11091C", +"w- c #130619", +"x- c #160A1E", +"y- c #43252C", +"z- c #F66439", +"A- c #FA6942", +"B- c #FD6C47", +"C- c #FE6E48", +"D- c #FE6F48", +"E- c #FE7049", +"F- c #FE714A", +"G- c #FE744A", +"H- c #FE7846", +"I- c #FD7243", +"J- c #FC703E", +"K- c #FA6C37", +"L- c #81312B", +"M- c #121123", +"N- c #15071D", +"O- c #16031A", +"P- c #17021B", +"Q- c #8F3D22", +"R- c #F8393E", +"S- c #E42A3D", +"T- c #E7473B", +"U- c #FB503B", +"V- c #FB4F3A", +"W- c #F95439", +"X- c #ED4C38", +"Y- c #F45938", +"Z- c #FB6537", +"`- c #EA5236", +" ; c #CE6232", +".; c #CD392C", +"+; c #181425", +"@; c #120F21", +"#; c #130D20", +"$; c #151225", +"%; c #903431", +"&; c #F8703D", +"*; c #FB6344", +"=; c #FD6748", +"-; c #FE6849", +";; c #FE6949", +">; c #FE6A49", +",; c #FE6C4A", +"'; c #FE704A", +"); c #FE734A", +"!; c #FE7449", +"~; c #FE7347", +"{; c #FE7145", +"]; c #FD6C42", +"^; c #FD753D", +"/; c #F36E35", +"(; c #CB452C", +"_; c #600D24", +":; c #1C061F", +"<; c #1E031F", +"[; c #5B3821", +"}; c #CE9822", +"|; c #FA4341", +"1; c #FB4341", +"2; c #FC4541", +"3; c #FC4542", +"4; c #FC4143", +"5; c #FC4D42", +"6; c #FB5042", +"7; c #FB5342", +"8; c #FC5242", +"9; c #FD4F40", +"0; c #FD503E", +"a; c #FB6339", +"b; c #F45E33", +"c; c #A12A2E", +"d; c #401E2C", +"e; c #452D2F", +"f; c #F74F38", +"g; c #FA5940", +"h; c #FC6245", +"i; c #FE6447", +"j; c #FE6449", +"k; c #FE6549", +"l; c #FE6749", +"m; c #FE6B49", +"n; c #FE6D49", +"o; c #FE6D48", +"p; c #FE6D47", +"q; c #FE6D45", +"r; c #FE6C44", +"s; c #FE6A42", +"t; c #FE663C", +"u; c #FC6233", +"v; c #752129", +"w; c #1F0922", +"x; c #750520", +"y; c #81061F", +"z; c #FA3D42", +"A; c #FB4142", +"B; c #FD4543", +"C; c #FD4844", +"D; c #FD4A45", +"E; c #FD4D45", +"F; c #FD5045", +"G; c #FD5345", +"H; c #FE5346", +"I; c #FE5445", +"J; c #FD5444", +"K; c #FC4F41", +"L; c #FA513D", +"M; c #F95339", +"N; c #F63736", +"O; c #F75737", +"P; c #F95F3B", +"Q; c #FB5840", +"R; c #FD5F43", +"S; c #FE6345", +"T; c #FE6547", +"U; c #FE6548", +"V; c #FE6448", +"W; c #FE6248", +"X; c #FE6348", +"Y; c #FE6748", +"Z; c #FE6848", +"`; c #FE6846", +" > c #FE6A45", +".> c #FE6D43", +"+> c #FE703F", +"@> c #FC6F36", +"#> c #6F302B", +"$> c #140A22", +"%> c #FA3B42", +"&> c #FC4243", +"*> c #FD4744", +"=> c #FE4A45", +"-> c #FE4C47", +";> c #FE4D47", +">> c #FE5047", +",> c #FE5347", +"'> c #FE5447", +")> c #FD5246", +"!> c #FB503F", +"~> c #FA543D", +"{> c #9B3D3B", +"]> c #A3433B", +"^> c #F9683D", +"/> c #FC6940", +"(> c #FE6342", +"_> c #FE6645", +":> c #FE6646", +"<> c #FE6147", +"[> c #FE6048", +"}> c #FE6148", +"|> c #FE6746", +"1> c #FE6A46", +"2> c #FE6F45", +"3> c #FE7441", +"4> c #FC7D39", +"5> c #6C422E", +"6> c #0F0F23", +"7> c #FA4142", +"8> c #FC4643", +"9> c #FE4D46", +"0> c #FE4E47", +"a> c #FE4F48", +"b> c #FE5148", +"c> c #FE5348", +"d> c #FE5548", +"e> c #FE5247", +"f> c #FD5445", +"g> c #FC5544", +"h> c #F96041", +"i> c #D33F3D", +"j> c #392D39", +"k> c #973C38", +"l> c #F94E3A", +"m> c #FD693E", +"n> c #FE6C43", +"o> c #FE6047", +"p> c #FE5D47", +"q> c #FE5E48", +"r> c #FE6948", +"s> c #FE6947", +"t> c #FE6B47", +"u> c #FE6E46", +"v> c #FD6D43", +"w> c #FB723D", +"x> c #D54A33", +"y> c #301C29", +"z> c #FB4A42", +"A> c #FD4B44", +"B> c #FE4F47", +"C> c #FE5048", +"D> c #FE5648", +"E> c #FE5848", +"F> c #FE5747", +"G> c #FE5547", +"H> c #FC5945", +"I> c #F95742", +"J> c #F3543D", +"K> c #A33336", +"L> c #302032", +"M> c #152433", +"N> c #CD3E38", +"O> c #FD5A3F", +"P> c #FE6343", +"Q> c #FE6446", +"R> c #FE6247", +"S> c #FE6A47", +"T> c #FC6542", +"U> c #FB6A3B", +"V> c #FA6D34", +"W> c #D73C2D", +"X> c #442428", +"Y> c #281323", +"Z> c #FD4E42", +"`> c #FD4D43", +" , c #FE4D45", +"., c #FE5248", +"+, c #FE5947", +"@, c #FE5C47", +"#, c #FE5B47", +"$, c #FE5A47", +"%, c #FE5847", +"&, c #FC5C45", +"*, c #F95B43", +"=, c #F3613F", +"-, c #E74F37", +";, c #8C2431", +">, c #161E2F", +",, c #CD4E33", +"', c #FD503A", +"), c #FE5D40", +"!, c #FE6445", +"~, c #FE6946", +"{, c #FE6847", +"], c #FE6747", +"^, c #FD6644", +"/, c #FD6241", +"(, c #FD5B3D", +"_, c #FE6739", +":, c #FE6135", +"<, c #AB4830", +"[, c #733E2A", +"}, c #161224", +"|, c #FC4E42", +"1, c #FE4D44", +"2, c #FE4E46", +"3, c #FE5147", +"4, c #FE5E47", +"5, c #FD5C46", +"6, c #FA5B44", +"7, c #F45441", +"8, c #EB393A", +"9, c #CC3433", +"0, c #47212F", +"a, c #59242F", +"b, c #FC6734", +"c, c #FC6F3A", +"d, c #FC723E", +"e, c #FD6540", +"f, c #FE6442", +"g, c #FE6643", +"h, c #FE6944", +"i, c #FE6546", +"j, c #FE6444", +"k, c #FE6143", +"l, c #FE5E41", +"m, c #FE613F", +"n, c #FE683C", +"o, c #FE7937", +"p, c #A25030", +"q, c #692629", +"r, c #151122", +"s, c #FA573F", +"t, c #FB4D40", +"u, c #FC4F43", +"v, c #FE5246", +"w, c #FF6347", +"x, c #FE5F48", +"y, c #F65942", +"z, c #F0493D", +"A, c #ED3736", +"B, c #73262F", +"C, c #10152C", +"D, c #3B292F", +"E, c #363034", +"F, c #AC3938", +"G, c #FC6B3B", +"H, c #FD763C", +"I, c #FE6D3F", +"J, c #FE6341", +"K, c #FE6642", +"L, c #FE6745", +"M, c #FE6245", +"N, c #FE6244", +"O, c #FE6841", +"P, c #FF683B", +"Q, c #EC7035", +"R, c #D0412D", +"S, c #3A1627", +"T, c #CF3938", +"U, c #F6543C", +"V, c #FB5040", +"W, c #FD5544", +"X, c #FE5A48", +"Y, c #FE5D48", +"Z, c #FE5F47", +"`, c #FF6147", +" ' c #FD5C45", +".' c #FB5B43", +"+' c #FA5A42", +"@' c #F76040", +"#' c #F4623D", +"$' c #F26D38", +"%' c #EC4130", +"&' c #380E2B", +"*' c #13122C", +"=' c #362D31", +"-' c #353435", +";' c #352E37", +">' c #2D3337", +",' c #CC5838", +"'' c #CD6F3A", +")' c #CE6E3D", +"!' c #FE793F", +"~' c #FD7541", +"{' c #FD6243", +"]' c #FE6545", +"^' c #FF6543", +"/' c #FF6240", +"(' c #FE723B", +"_' c #FE8034", +":' c #442D2C", +"<' c #311725", +"[' c #222830", +"}' c #B73B36", +"|' c #F94C3D", +"1' c #FD5543", +"2' c #FE5B48", +"3' c #FF5E47", +"4' c #FE5C48", +"5' c #FC5B44", +"6' c #F95640", +"7' c #C34E3D", +"8' c #A45A3A", +"9' c #F37438", +"0' c #F28935", +"a' c #AF422F", +"b' c #240D2B", +"c' c #88292F", +"d' c #FA8E34", +"e' c #FC7E38", +"f' c #FC5939", +"g' c #694A37", +"h' c #693437", +"i' c #382638", +"j' c #142439", +"k' c #9F483A", +"l' c #C45E3C", +"m' c #FD7240", +"n' c #FF6645", +"o' c #FF6245", +"p' c #FF6045", +"q' c #FF6146", +"r' c #FF6246", +"s' c #FF6446", +"t' c #FF6545", +"u' c #FE763F", +"v' c #FE7237", +"w' c #C65331", +"x' c #3D272A", +"y' c #0D1E2B", +"z' c #683032", +"A' c #F9453A", +"B' c #FD5341", +"C' c #FE5A46", +"D' c #FF5A48", +"E' c #FE5948", +"F' c #FD5A47", +"G' c #FC5D43", +"H' c #F95B3D", +"I' c #713F37", +"J' c #1E2D32", +"K' c #C44531", +"L' c #EF7A2F", +"M' c #6B2E2C", +"N' c #0F0E2C", +"O' c #F56633", +"P' c #FA803A", +"Q' c #FC673E", +"R' c #FD673E", +"S' c #FC6F3C", +"T' c #FA6E3B", +"U' c #C6633A", +"V' c #A06739", +"W' c #835638", +"X' c #381F38", +"Y' c #713B38", +"Z' c #7B503C", +"`' c #FE7741", +" ) c #FE7344", +".) c #FE6D46", +"+) c #FF6946", +"@) c #FF5E46", +"#) c #FF5D46", +"$) c #FF5D47", +"%) c #FF5F48", +"&) c #FF6248", +"*) c #FE6941", +"=) c #FC783C", +"-) c #C46B35", +";) c #892730", +">) c #111629", +",) c #1F2630", +"') c #AD3939", +")) c #FC5D41", +"!) c #FE5946", +"~) c #FF5848", +"{) c #FE5549", +"]) c #FC5E42", +"^) c #FA673B", +"/) c #DB7033", +"() c #392E2B", +"_) c #311A28", +":) c #3C2127", +"<) c #1D1027", +"[) c #92102C", +"}) c #F58336", +"|) c #FA673E", +"1) c #FD6642", +"2) c #FD5A41", +"3) c #FC6D41", +"4) c #FC6D3F", +"5) c #FD683E", +"6) c #F38C39", +"7) c #CE6535", +"8) c #612E34", +"9) c #1D2637", +"0) c #71513E", +"a) c #FF6847", +"b) c #FF5F47", +"c) c #FF5A46", +"d) c #FF5847", +"e) c #FF5748", +"f) c #FF594A", +"g) c #FF5E4B", +"h) c #FE654C", +"i) c #FE694B", +"j) c #FE6B48", +"k) c #FC6A43", +"l) c #F7683E", +"m) c #EC6E39", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" . + @ # $ % ", +" & * = - ; > , ' ) ! ~ ", +" { ] ^ / ( _ : < [ } | 1 2 ", +" 3 4 5 6 7 8 9 0 a b c d e f g h i j ", +" k l m n o p q r s t u v w x y z A B C D ", +" E F G H I J K L M N O P Q R S T U V W X Y Z ` ", +" ...+.@.#.$.%.&.*.=.-.;.>.,.S '.).!.~.{.].^./.(._. ", +" :.<.[.}.|.1.2.3.4.5.6.7.8.9.0.a.b.c.d.e.!.S f.g.h.i.j.k. ", +" l.m.n.o.p.q.r.s.t.u.J v.w.x.y.z.A.c.d.d.B.C.D.E.F.G.H.I. ", +" J.K.L.M.N.O.P.Q.R.t S.T.U.V.W.X.Y.Z.`. +d.d..+B.'.++@+#+$+%+ ", +" &+*+=+-+;+>+,+'+)+!+~+{+]+^+/+(+_+:+<+[+}+|+1+d.2+3+4+d.5+6+7+8+9+0+ ", +" a+b+c+d+e+f+g+h+i+j+k+l+m+n+^+o+p+q+r+s+t+u+v+b.w+x+y+z+A+w+B+C+D+E+F+G+ ", +" H+I+J+K+L+M+N+O+P+Q+R+S+T+U+V+W+Q ,.X+Y+Z+`+ @.@+@@@#@$@%@&@*@=@#@-@;@>@,@'@ ", +" )@!@~@{@]@^@/@(@_@:@<@[@}@|@1@2@3@R ,.4@5@6@7@8@9@0@a@#@b@c@=@d@e@f@g@>@h@i@j@ ", +" k@l@m@n@o@p@q@r@s@t@u@v@w@x@y@^+R S z@A@z.q+B@C@D@E@F@G@H@#@e@#@#@f@g@I@J@K@L@ ", +" M@N@O@P@Q@R@S@T@U@V@W@X@Y@Z@`@ #.#+#+#S A@@###$#%#&#*#=#-#f@B+B+B+f@;#>#,#'#)# ", +" !#~#{#]#^#/#(#(#_#:#<#[#}#|#1#^+.#S +#+#z@2#3#4#5#6#7#8#9#0#A.B+B+a#A.@@b#c#d# ", +" e#f#g#h#i#j#k#l#m#n#o#p#q#r#s#t#u#v#.#w#S R ^+x#y#z#A#B#C#D#-#A.a#`.`.b.g@E#d#F# ", +" G#0@H#I#J#K#L#M#N#O#P#Q#R#S#T#U#V#>.W#3@v#R R X+X#Y#s#Z#`# $.$+$@$g@f@5+5+#$6+$$%$&$ ", +" *$=$-$;$>$,$'$)$!$~${$]$^$/$($_$*$u#:$Q 3@,.X+z.<$[$}$|$1$2$3$4$5$6$7$e@8$#$G@9$0$a$ ", +" ,.4@E#b$c$d$e$f$g$h$i$j$k$l$m$n$`@>.:$o$3@,. #a.p$q$r$s$t$u$v$w$x$y$z$A$B$#@C$D$E$F$G$ ", +" R S H$v+I$J$K$n+L$:$o$o$M$N$L$O$P$Q$R$N$o$3@S$T$U$V$W$X$Y$Z$`$ %.%+%@%#%$%%%&%*%=%-%;%>% ", +" E.,%~.'%Z.4@v W#o$)%)%)%Q !%~%{%]%^%Q$u u#/%(%_%:%<%[%}%|%1%2%3%4%=%5%6%7%8%9%0%a%b%c%d% ", +" e%f%g%a#,%,%z@R 3@3@3@)%Q h%i%j%k%l%m%{+n%o%p%q%r%s%t%u%v%w%x%y%z%A%*%B%C%D%E%F%G%H%I% ", +" J%K%L%M%N%D.S v#)%)%O%P%Q%R%S%T%U%V%W%X%Y%Z%`% &.&+&@&#&$&%&&&*&f@a##@=&-&;&>&,&'&)& ", +" !&~&{&]&^&.#w#^&/%/&(&_&:&<&[&}&|&1&2&3&4&5&6&7&8&9&0&a&b&c&d&e&e@1+5+e@f&g&h&i&j& ", +" k&l&m&n&o&p&q&r&i%s&3.t&u&v&w&x&y&z&A&B&C&D&E&F&G&H&I&J&K&L&M&N&O&P&1+`.e@f&Q&R&S&T& ", +" 0 U&V&W&X&<&Y&j%Z&`& *.*+*@*#*$*%*&***=*-*;*>*>*,*'*)*!*~*{*]*^*/*(*a#B+#@_*:*<*[*}* ", +" |*1*2*3*4*5*6*7*8*9*0*a*b*c*d*e*f*g*h*i*j*k*l*m*n*o*p*q*r*s*t*u*v*E.w*d.e@x*y*z*A*B* ", +" C*D*E*F*G*H*I*J*K*L*M*N*O*P*Q*R*S*T*U*V*W*l*X*o*o*Y*Z*`* =.=+=@=#='%$=%=e@&=*===-=;= ", +" >=,='=)=!=~={=]=^=/=(=_=:=<=[=}=|=1=2=3=4=5=p*6=6=7=8=9=0=a=b=c=d=A@~.b.B+e=f=g=h=i= ", +" j=k=l=m=n=o=p=q=r=s=t=u=v=w=x=y=z=A=5=Z*B=C=D=E=8=F=G=H=I=J=K=S$R z@'%L=M=N=O= ", +" P=Q=R=S=T=U=V=W=X=Y=Z=`= -.-+-@-#-$-5=p*E=D=%-%-q*&-*-=---;->-,-/%3@^+'-)-!-~- ", +" {-]-^-/-(-_-:-<-[-}-|-1-2- -3-4-5-6-7-8-n*m*9-0-9-o*a-b-c-d-e-f-g-(&h%w c h-i- ", +" j-k-l-m-n-o-p-q-r-s-t-u-v-w-,-x-y-z-A-B-C-D-E-E-F-G-_@m*H-I-J-K-L-M-N-O-P-(+Q- ", +" R-S-T-U-V-W-X-Y-Z-`- ;.;+;@;#;$;%;&;*;=;-;-;;;>;,;';);!;~;{;];^;/;(;_;:;<;[;}; ", +" |;1;2;3;4;5;6;7;8;9;0;a;b;c;d;e;f;g;h;i;j;j;k;k;l;m;n;o;p;q;r;s;t;u;v;w;x;y; ", +" z;A;B;C;D;E;F;G;H;I;J;K;L;M;N;O;P;Q;R;S;T;U;V;W;X;k;Y;Z;`; >r;.>+>@>#>$> ", +" %>&>*>=>->;>>>,>'>,>)>F;8;!>~>{>]>^>/>(>_>:>i;<>[>X;}>i;|>1>q;2>3>4>5>6> ", +" 7>8>=>9>0>a>b>c>d>,>e>e>f>g>h>i>j>k>l>m>n>:>i;o>p>q>W;r>s>t>p;u>v>w>x>y> ", +" z>A>9>0>B>C>c>D>E>F>G>G>F>H>I>J>K>L>M>N>O>P>Q>R>o>R>T;s>S>S>S>t>1>T>U>V>W>X>Y> ", +" Z>`> ,9>B>.,D>+,@,#,$,%,$,&,*,=,-,;,>,,,',),P>!,!,_>~,t>s>{,],{,],^,/,(,_,:,<,[,}, ", +" |,`>1,2,3,G>+,4,o>o>4,@,@,5,6,7,8,9,0,a,b,c,d,e,f,g,h, >~,|>T;T;T;i,j,k,l,m,n,o,p,q,r, ", +" s,t,u,v,G>%,@,o>w,R>x,p>@,5,6,y,z,A,B,C,D,E,F,G,H,I,J,K,L,L,i,i;i;i;Q>S;M,N,P>O,P,Q,R,S, ", +" T,U,V,W,%,X,Y,Z,`,[>q>@, '.'+'@'#'$'%'&'*'='-';'>',''')'!'~'{'N,i,:>_>]'M,M,Q>_>^'/'('_':'<' ", +" ['}'|'1'$,X,2'p>3'4'2'@,5'6'7'8'9'0'a'b'c'd'e'f'g'h'i'j'k'l'd,m'g, > >n'o'p'q'r's't'.>u'v'w'x' ", +" y'z'A'B'C'X,X,2'D'E'E'F'G'H'I'J'K'L'M'N'O'P'Q'R'S'T'U'V'W'X'Y'Z'`' ).)+)r'@)#)$)%)&)l;1>*)=)-);) ", +" >),)')))!)X,E'X,~){)d>!)])^)/)()_):)<)[)})|)1)f,2)3)4)5)6)7)8)9)0)*--*a)b)c)d)e)f)g)h)i)j)k)l)m) ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/lib/json/pure.rb b/lib/json/pure.rb new file mode 100644 index 0000000..565ef0c --- /dev/null +++ b/lib/json/pure.rb @@ -0,0 +1,77 @@ +require 'json/common' +require 'json/pure/parser' +require 'json/pure/generator' + +module JSON + begin + require 'iconv' + # An iconv instance to convert from UTF8 to UTF16 Big Endian. + UTF16toUTF8 = Iconv.new('utf-8', 'utf-16be') # :nodoc: + # An iconv instance to convert from UTF16 Big Endian to UTF8. + UTF8toUTF16 = Iconv.new('utf-16be', 'utf-8') # :nodoc: + UTF8toUTF16.iconv('no bom') + rescue LoadError + raise MissingUnicodeSupport, + "iconv couldn't be loaded, which is required for UTF-8/UTF-16 conversions" + rescue Errno::EINVAL, Iconv::InvalidEncoding + # Iconv doesn't support big endian utf-16. Let's try to hack this manually + # into the converters. + begin + old_verbose, $VERBSOSE = $VERBOSE, nil + # An iconv instance to convert from UTF8 to UTF16 Big Endian. + UTF16toUTF8 = Iconv.new('utf-8', 'utf-16') # :nodoc: + # An iconv instance to convert from UTF16 Big Endian to UTF8. + UTF8toUTF16 = Iconv.new('utf-16', 'utf-8') # :nodoc: + UTF8toUTF16.iconv('no bom') + if UTF8toUTF16.iconv("\xe2\x82\xac") == "\xac\x20" + swapper = Class.new do + def initialize(iconv) # :nodoc: + @iconv = iconv + end + + def iconv(string) # :nodoc: + result = @iconv.iconv(string) + JSON.swap!(result) + end + end + UTF8toUTF16 = swapper.new(UTF8toUTF16) # :nodoc: + end + if UTF16toUTF8.iconv("\xac\x20") == "\xe2\x82\xac" + swapper = Class.new do + def initialize(iconv) # :nodoc: + @iconv = iconv + end + + def iconv(string) # :nodoc: + string = JSON.swap!(string.dup) + @iconv.iconv(string) + end + end + UTF16toUTF8 = swapper.new(UTF16toUTF8) # :nodoc: + end + rescue Errno::EINVAL, Iconv::InvalidEncoding + raise MissingUnicodeSupport, "iconv doesn't seem to support UTF-8/UTF-16 conversions" + ensure + $VERBOSE = old_verbose + end + end + + # Swap consecutive bytes of _string_ in place. + def self.swap!(string) # :nodoc: + 0.upto(string.size / 2) do |i| + break unless string[2 * i + 1] + string[2 * i], string[2 * i + 1] = string[2 * i + 1], string[2 * i] + end + string + end + + # This module holds all the modules/classes that implement JSON's + # functionality in pure ruby. + module Pure + $DEBUG and warn "Using pure library for JSON." + JSON.parser = Parser + JSON.generator = Generator + end + + JSON_LOADED = true +end diff --git a/lib/json/pure/generator.rb b/lib/json/pure/generator.rb new file mode 100644 index 0000000..d51316e --- /dev/null +++ b/lib/json/pure/generator.rb @@ -0,0 +1,429 @@ +module JSON + MAP = { + "\x0" => '\u0000', + "\x1" => '\u0001', + "\x2" => '\u0002', + "\x3" => '\u0003', + "\x4" => '\u0004', + "\x5" => '\u0005', + "\x6" => '\u0006', + "\x7" => '\u0007', + "\b" => '\b', + "\t" => '\t', + "\n" => '\n', + "\xb" => '\u000b', + "\f" => '\f', + "\r" => '\r', + "\xe" => '\u000e', + "\xf" => '\u000f', + "\x10" => '\u0010', + "\x11" => '\u0011', + "\x12" => '\u0012', + "\x13" => '\u0013', + "\x14" => '\u0014', + "\x15" => '\u0015', + "\x16" => '\u0016', + "\x17" => '\u0017', + "\x18" => '\u0018', + "\x19" => '\u0019', + "\x1a" => '\u001a', + "\x1b" => '\u001b', + "\x1c" => '\u001c', + "\x1d" => '\u001d', + "\x1e" => '\u001e', + "\x1f" => '\u001f', + '"' => '\"', + '\\' => '\\\\', + } # :nodoc: + + # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with + # UTF16 big endian characters as \u????, and return it. + if String.method_defined?(:force_encoding) + def utf8_to_json(string) # :nodoc: + string = string.dup + string << '' # XXX workaround: avoid buffer sharing + string.force_encoding(Encoding::ASCII_8BIT) + string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] } + string.gsub!(/( + (?: + [\xc2-\xdf][\x80-\xbf] | + [\xe0-\xef][\x80-\xbf]{2} | + [\xf0-\xf4][\x80-\xbf]{3} + )+ | + [\x80-\xc1\xf5-\xff] # invalid + )/nx) { |c| + c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'" + s = JSON::UTF8toUTF16.iconv(c).unpack('H*')[0] + s.gsub!(/.{4}/n, '\\\\u\&') + } + string.force_encoding(Encoding::UTF_8) + string + rescue Iconv::Failure => e + raise GeneratorError, "Caught #{e.class}: #{e}" + end + else + def utf8_to_json(string) # :nodoc: + string = string.gsub(/["\\\x0-\x1f]/) { MAP[$&] } + string.gsub!(/( + (?: + [\xc2-\xdf][\x80-\xbf] | + [\xe0-\xef][\x80-\xbf]{2} | + [\xf0-\xf4][\x80-\xbf]{3} + )+ | + [\x80-\xc1\xf5-\xff] # invalid + )/nx) { |c| + c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'" + s = JSON::UTF8toUTF16.iconv(c).unpack('H*')[0] + s.gsub!(/.{4}/n, '\\\\u\&') + } + string + rescue Iconv::Failure => e + raise GeneratorError, "Caught #{e.class}: #{e}" + end + end + module_function :utf8_to_json + + module Pure + module Generator + # This class is used to create State instances, that are use to hold data + # while generating a JSON text from a a Ruby data structure. + class State + # Creates a State object from _opts_, which ought to be Hash to create + # a new State instance configured by _opts_, something else to create + # an unconfigured instance. If _opts_ is a State object, it is just + # returned. + def self.from_state(opts) + case opts + when self + opts + when Hash + new(opts) + else + new + end + end + + # Instantiates a new State object, configured by _opts_. + # + # _opts_ can have the following keys: + # + # * *indent*: a string used to indent levels (default: ''), + # * *space*: a string that is put after, a : or , delimiter (default: ''), + # * *space_before*: a string that is put before a : pair delimiter (default: ''), + # * *object_nl*: a string that is put at the end of a JSON object (default: ''), + # * *array_nl*: a string that is put at the end of a JSON array (default: ''), + # * *check_circular*: true if checking for circular data structures + # should be done (the default), false otherwise. + # * *check_circular*: true if checking for circular data structures + # should be done, false (the default) otherwise. + # * *allow_nan*: true if NaN, Infinity, and -Infinity should be + # generated, otherwise an exception is thrown, if these values are + # encountered. This options defaults to false. + def initialize(opts = {}) + @seen = {} + @indent = '' + @space = '' + @space_before = '' + @object_nl = '' + @array_nl = '' + @check_circular = true + @allow_nan = false + configure opts + end + + # This string is used to indent levels in the JSON text. + attr_accessor :indent + + # This string is used to insert a space between the tokens in a JSON + # string. + attr_accessor :space + + # This string is used to insert a space before the ':' in JSON objects. + attr_accessor :space_before + + # This string is put at the end of a line that holds a JSON object (or + # Hash). + attr_accessor :object_nl + + # This string is put at the end of a line that holds a JSON array. + attr_accessor :array_nl + + # This integer returns the maximum level of data structure nesting in + # the generated JSON, max_nesting = 0 if no maximum is checked. + attr_accessor :max_nesting + + def check_max_nesting(depth) # :nodoc: + return if @max_nesting.zero? + current_nesting = depth + 1 + current_nesting > @max_nesting and + raise NestingError, "nesting of #{current_nesting} is too deep" + end + + # Returns true, if circular data structures should be checked, + # otherwise returns false. + def check_circular? + @check_circular + end + + # Returns true if NaN, Infinity, and -Infinity should be considered as + # valid JSON and output. + def allow_nan? + @allow_nan + end + + # Returns _true_, if _object_ was already seen during this generating + # run. + def seen?(object) + @seen.key?(object.__id__) + end + + # Remember _object_, to find out if it was already encountered (if a + # cyclic data structure is if a cyclic data structure is rendered). + def remember(object) + @seen[object.__id__] = true + end + + # Forget _object_ for this generating run. + def forget(object) + @seen.delete object.__id__ + end + + # Configure this State instance with the Hash _opts_, and return + # itself. + def configure(opts) + @indent = opts[:indent] if opts.key?(:indent) + @space = opts[:space] if opts.key?(:space) + @space_before = opts[:space_before] if opts.key?(:space_before) + @object_nl = opts[:object_nl] if opts.key?(:object_nl) + @array_nl = opts[:array_nl] if opts.key?(:array_nl) + @check_circular = !!opts[:check_circular] if opts.key?(:check_circular) + @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan) + if !opts.key?(:max_nesting) # defaults to 19 + @max_nesting = 19 + elsif opts[:max_nesting] + @max_nesting = opts[:max_nesting] + else + @max_nesting = 0 + end + self + end + + # Returns the configuration instance variables as a hash, that can be + # passed to the configure method. + def to_h + result = {} + for iv in %w[indent space space_before object_nl array_nl check_circular allow_nan max_nesting] + result[iv.intern] = instance_variable_get("@#{iv}") + end + result + end + end + + module GeneratorMethods + module Object + # Converts this object to a string (calling #to_s), converts + # it to a JSON string, and returns the result. This is a fallback, if no + # special method #to_json was defined for some object. + def to_json(*) to_s.to_json end + end + + module Hash + # Returns a JSON string containing a JSON object, that is unparsed from + # this Hash instance. + # _state_ is a JSON::State object, that can also be used to configure the + # produced JSON string output further. + # _depth_ is used to find out nesting depth, to indent accordingly. + def to_json(state = nil, depth = 0, *) + if state + state = JSON.state.from_state(state) + state.check_max_nesting(depth) + json_check_circular(state) { json_transform(state, depth) } + else + json_transform(state, depth) + end + end + + private + + def json_check_circular(state) + if state and state.check_circular? + state.seen?(self) and raise JSON::CircularDatastructure, + "circular data structures not supported!" + state.remember self + end + yield + ensure + state and state.forget self + end + + def json_shift(state, depth) + state and not state.object_nl.empty? or return '' + state.indent * depth + end + + def json_transform(state, depth) + delim = ',' + if state + delim << state.object_nl + result = '{' + result << state.object_nl + result << map { |key,value| + s = json_shift(state, depth + 1) + s << key.to_s.to_json(state, depth + 1) + s << state.space_before + s << ':' + s << state.space + s << value.to_json(state, depth + 1) + }.join(delim) + result << state.object_nl + result << json_shift(state, depth) + result << '}' + else + result = '{' + result << map { |key,value| + key.to_s.to_json << ':' << value.to_json + }.join(delim) + result << '}' + end + result + end + end + + module Array + # Returns a JSON string containing a JSON array, that is unparsed from + # this Array instance. + # _state_ is a JSON::State object, that can also be used to configure the + # produced JSON string output further. + # _depth_ is used to find out nesting depth, to indent accordingly. + def to_json(state = nil, depth = 0, *) + if state + state = JSON.state.from_state(state) + state.check_max_nesting(depth) + json_check_circular(state) { json_transform(state, depth) } + else + json_transform(state, depth) + end + end + + private + + def json_check_circular(state) + if state and state.check_circular? + state.seen?(self) and raise JSON::CircularDatastructure, + "circular data structures not supported!" + state.remember self + end + yield + ensure + state and state.forget self + end + + def json_shift(state, depth) + state and not state.array_nl.empty? or return '' + state.indent * depth + end + + def json_transform(state, depth) + delim = ',' + if state + delim << state.array_nl + result = '[' + result << state.array_nl + result << map { |value| + json_shift(state, depth + 1) << value.to_json(state, depth + 1) + }.join(delim) + result << state.array_nl + result << json_shift(state, depth) + result << ']' + else + '[' << map { |value| value.to_json }.join(delim) << ']' + end + end + end + + module Integer + # Returns a JSON string representation for this Integer number. + def to_json(*) to_s end + end + + module Float + # Returns a JSON string representation for this Float number. + def to_json(state = nil, *) + case + when infinite? + if !state || state.allow_nan? + to_s + else + raise GeneratorError, "#{self} not allowed in JSON" + end + when nan? + if !state || state.allow_nan? + to_s + else + raise GeneratorError, "#{self} not allowed in JSON" + end + else + to_s + end + end + end + + module String + # This string should be encoded with UTF-8 A call to this method + # returns a JSON string encoded with UTF16 big endian characters as + # \u????. + def to_json(*) + '"' << JSON.utf8_to_json(self) << '"' + end + + # Module that holds the extinding methods if, the String module is + # included. + module Extend + # Raw Strings are JSON Objects (the raw bytes are stored in an array for the + # key "raw"). The Ruby String can be created by this module method. + def json_create(o) + o['raw'].pack('C*') + end + end + + # Extends _modul_ with the String::Extend module. + def self.included(modul) + modul.extend Extend + end + + # This method creates a raw object hash, that can be nested into + # other data structures and will be unparsed as a raw string. This + # method should be used, if you want to convert raw strings to JSON + # instead of UTF-8 strings, e. g. binary data. + def to_json_raw_object + { + JSON.create_id => self.class.name, + 'raw' => self.unpack('C*'), + } + end + + # This method creates a JSON text from the result of + # a call to to_json_raw_object of this String. + def to_json_raw(*args) + to_json_raw_object.to_json(*args) + end + end + + module TrueClass + # Returns a JSON string for true: 'true'. + def to_json(*) 'true' end + end + + module FalseClass + # Returns a JSON string for false: 'false'. + def to_json(*) 'false' end + end + + module NilClass + # Returns a JSON string for nil: 'null'. + def to_json(*) 'null' end + end + end + end + end +end diff --git a/lib/json/pure/parser.rb b/lib/json/pure/parser.rb new file mode 100644 index 0000000..7e8fe08 --- /dev/null +++ b/lib/json/pure/parser.rb @@ -0,0 +1,269 @@ +require 'strscan' + +module JSON + module Pure + # This class implements the JSON parser that is used to parse a JSON string + # into a Ruby data structure. + class Parser < StringScanner + STRING = /" ((?:[^\x0-\x1f"\\] | + # escaped special characters: + \\["\\\/bfnrt] | + \\u[0-9a-fA-F]{4} | + # match all but escaped special characters: + \\[\x20-\x21\x23-\x2e\x30-\x5b\x5d-\x61\x63-\x65\x67-\x6d\x6f-\x71\x73\x75-\xff])*) + "/nx + INTEGER = /(-?0|-?[1-9]\d*)/ + FLOAT = /(-? + (?:0|[1-9]\d*) + (?: + \.\d+(?i:e[+-]?\d+) | + \.\d+ | + (?i:e[+-]?\d+) + ) + )/x + NAN = /NaN/ + INFINITY = /Infinity/ + MINUS_INFINITY = /-Infinity/ + OBJECT_OPEN = /\{/ + OBJECT_CLOSE = /\}/ + ARRAY_OPEN = /\[/ + ARRAY_CLOSE = /\]/ + PAIR_DELIMITER = /:/ + COLLECTION_DELIMITER = /,/ + TRUE = /true/ + FALSE = /false/ + NULL = /null/ + IGNORE = %r( + (?: + //[^\n\r]*[\n\r]| # line comments + /\* # c-style comments + (?: + [^*/]| # normal chars + /[^*]| # slashes that do not start a nested comment + \*[^/]| # asterisks that do not end this comment + /(?=\*/) # single slash before this comment's end + )* + \*/ # the End of this comment + |[ \t\r\n]+ # whitespaces: space, horicontal tab, lf, cr + )+ + )mx + + UNPARSED = Object.new + + # Creates a new JSON::Pure::Parser instance for the string _source_. + # + # It will be configured by the _opts_ hash. _opts_ can have the following + # keys: + # * *max_nesting*: The maximum depth of nesting allowed in the parsed data + # structures. Disable depth checking with :max_nesting => false|nil|0, + # it defaults to 19. + # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in + # defiance of RFC 4627 to be parsed by the Parser. This option defaults + # to false. + # * *create_additions*: If set to false, the Parser doesn't create + # additions even if a matchin class and create_id was found. This option + # defaults to true. + # * *object_class*: Defaults to Hash + # * *array_class*: Defaults to Array + def initialize(source, opts = {}) + super + if !opts.key?(:max_nesting) # defaults to 19 + @max_nesting = 19 + elsif opts[:max_nesting] + @max_nesting = opts[:max_nesting] + else + @max_nesting = 0 + end + @allow_nan = !!opts[:allow_nan] + ca = true + ca = opts[:create_additions] if opts.key?(:create_additions) + @create_id = ca ? JSON.create_id : nil + @object_class = opts[:object_class] || Hash + @array_class = opts[:array_class] || Array + end + + alias source string + + # Parses the current JSON string _source_ and returns the complete data + # structure as a result. + def parse + reset + obj = nil + until eos? + case + when scan(OBJECT_OPEN) + obj and raise ParserError, "source '#{peek(20)}' not in JSON!" + @current_nesting = 1 + obj = parse_object + when scan(ARRAY_OPEN) + obj and raise ParserError, "source '#{peek(20)}' not in JSON!" + @current_nesting = 1 + obj = parse_array + when skip(IGNORE) + ; + else + raise ParserError, "source '#{peek(20)}' not in JSON!" + end + end + obj or raise ParserError, "source did not contain any JSON!" + obj + end + + private + + # Unescape characters in strings. + UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr } + UNESCAPE_MAP.update({ + ?" => '"', + ?\\ => '\\', + ?/ => '/', + ?b => "\b", + ?f => "\f", + ?n => "\n", + ?r => "\r", + ?t => "\t", + ?u => nil, + }) + + def parse_string + if scan(STRING) + return '' if self[1].empty? + string = self[1].gsub(%r((?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff]))n) do |c| + if u = UNESCAPE_MAP[$&[1]] + u + else # \uXXXX + bytes = '' + i = 0 + while c[6 * i] == ?\\ && c[6 * i + 1] == ?u + bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16) + i += 1 + end + JSON::UTF16toUTF8.iconv(bytes) + end + end + if string.respond_to?(:force_encoding) + string.force_encoding(Encoding::UTF_8) + end + string + else + UNPARSED + end + rescue Iconv::Failure => e + raise GeneratorError, "Caught #{e.class}: #{e}" + end + + def parse_value + case + when scan(FLOAT) + Float(self[1]) + when scan(INTEGER) + Integer(self[1]) + when scan(TRUE) + true + when scan(FALSE) + false + when scan(NULL) + nil + when (string = parse_string) != UNPARSED + string + when scan(ARRAY_OPEN) + @current_nesting += 1 + ary = parse_array + @current_nesting -= 1 + ary + when scan(OBJECT_OPEN) + @current_nesting += 1 + obj = parse_object + @current_nesting -= 1 + obj + when @allow_nan && scan(NAN) + NaN + when @allow_nan && scan(INFINITY) + Infinity + when @allow_nan && scan(MINUS_INFINITY) + MinusInfinity + else + UNPARSED + end + end + + def parse_array + raise NestingError, "nesting of #@current_nesting is too deep" if + @max_nesting.nonzero? && @current_nesting > @max_nesting + result = @array_class.new + delim = false + until eos? + case + when (value = parse_value) != UNPARSED + delim = false + result << value + skip(IGNORE) + if scan(COLLECTION_DELIMITER) + delim = true + elsif match?(ARRAY_CLOSE) + ; + else + raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!" + end + when scan(ARRAY_CLOSE) + if delim + raise ParserError, "expected next element in array at '#{peek(20)}'!" + end + break + when skip(IGNORE) + ; + else + raise ParserError, "unexpected token in array at '#{peek(20)}'!" + end + end + result + end + + def parse_object + raise NestingError, "nesting of #@current_nesting is too deep" if + @max_nesting.nonzero? && @current_nesting > @max_nesting + result = @object_class.new + delim = false + until eos? + case + when (string = parse_string) != UNPARSED + skip(IGNORE) + unless scan(PAIR_DELIMITER) + raise ParserError, "expected ':' in object at '#{peek(20)}'!" + end + skip(IGNORE) + unless (value = parse_value).equal? UNPARSED + result[string] = value + delim = false + skip(IGNORE) + if scan(COLLECTION_DELIMITER) + delim = true + elsif match?(OBJECT_CLOSE) + ; + else + raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!" + end + else + raise ParserError, "expected value in object at '#{peek(20)}'!" + end + when scan(OBJECT_CLOSE) + if delim + raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!" + end + if @create_id and klassname = result[@create_id] + klass = JSON.deep_const_get klassname + break unless klass and klass.json_creatable? + result = klass.json_create(result) + end + break + when skip(IGNORE) + ; + else + raise ParserError, "unexpected token in object at '#{peek(20)}'!" + end + end + result + end + end + end +end diff --git a/lib/json/version.rb b/lib/json/version.rb new file mode 100644 index 0000000..28bea76 --- /dev/null +++ b/lib/json/version.rb @@ -0,0 +1,8 @@ +module JSON + # JSON version + VERSION = '1.1.8' + VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc: + VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc: + VERSION_MINOR = VERSION_ARRAY[1] # :nodoc: + VERSION_BUILD = VERSION_ARRAY[2] # :nodoc: +end |