From 4a84c27e3ee50c4989591fcde2b7a10c5ccc0395 Mon Sep 17 00:00:00 2001 From: naruse Date: Wed, 1 Dec 2010 16:26:13 +0000 Subject: * ext/json: Update github/flori/json from 1.4.2+ to e22b2f2bdfe6a9b0. this fixes some bugs. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@30003 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ext/json/lib/json/common.rb | 35 +- ext/json/lib/json/editor.rb | 1371 ------------------------------------------ ext/json/lib/json/ext.rb | 4 +- ext/json/lib/json/version.rb | 2 +- 4 files changed, 19 insertions(+), 1393 deletions(-) delete mode 100644 ext/json/lib/json/editor.rb (limited to 'ext/json/lib') diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb index 244634b7a1..d5444b92c8 100644 --- a/ext/json/lib/json/common.rb +++ b/ext/json/lib/json/common.rb @@ -11,9 +11,9 @@ module JSON # generate and parse for their documentation. def [](object, opts = {}) if object.respond_to? :to_str - JSON.parse(object.to_str, opts => {}) + JSON.parse(object.to_str, opts) else - JSON.generate(object, opts => {}) + JSON.generate(object, opts) end end @@ -40,8 +40,8 @@ module JSON else begin p.const_missing(c) - rescue NameError - raise ArgumentError, "can't find const #{path}" + rescue NameError => e + raise ArgumentError, "can't get const #{path}: #{e}" end end end @@ -49,6 +49,7 @@ module JSON # Set the module _generator_ to be used by JSON. def generator=(generator) # :nodoc: + old, $VERBOSE = $VERBOSE, nil @generator = generator generator_methods = generator::GeneratorMethods for const in generator_methods.constants @@ -63,20 +64,22 @@ module JSON end self.state = generator::State const_set :State, self.state - const_set :SAFE_STATE_PROTOTYPE, State.new.freeze + const_set :SAFE_STATE_PROTOTYPE, State.new const_set :FAST_STATE_PROTOTYPE, State.new( :indent => '', :space => '', :object_nl => "", :array_nl => "", :max_nesting => false - ).freeze + ) const_set :PRETTY_STATE_PROTOTYPE, State.new( :indent => ' ', :space => ' ', :object_nl => "\n", :array_nl => "\n" - ).freeze + ) + ensure + $VERBOSE = old end # Returns the JSON generator modul, that is used by JSON. This might be @@ -196,6 +199,7 @@ module JSON # amount of sanity checks, and the pretty_generate method for some # defaults for a pretty output. def generate(obj, opts = nil) + state = SAFE_STATE_PROTOTYPE.dup if opts if opts.respond_to? :to_hash opts = opts.to_hash @@ -204,10 +208,7 @@ module JSON else raise TypeError, "can't convert #{opts.class} into Hash" end - state = SAFE_STATE_PROTOTYPE.dup state = state.configure(opts) - else - state = SAFE_STATE_PROTOTYPE end state.generate(obj) end @@ -225,6 +226,7 @@ module JSON # *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, opts = nil) + state = FAST_STATE_PROTOTYPE.dup if opts if opts.respond_to? :to_hash opts = opts.to_hash @@ -233,10 +235,7 @@ module JSON else raise TypeError, "can't convert #{opts.class} into Hash" end - state = FAST_STATE_PROTOTYPE.dup state.configure(opts) - else - state = FAST_STATE_PROTOTYPE end state.generate(obj) end @@ -254,6 +253,7 @@ module JSON # 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 = PRETTY_STATE_PROTOTYPE.dup if opts if opts.respond_to? :to_hash opts = opts.to_hash @@ -262,10 +262,7 @@ module JSON else raise TypeError, "can't convert #{opts.class} into Hash" end - state = PRETTY_STATE_PROTOTYPE.dup state.configure(opts) - else - state = PRETTY_STATE_PROTOTYPE end state.generate(obj) end @@ -377,11 +374,11 @@ module ::Kernel # # The _opts_ argument is passed through to generate/parse respectively, see # generate and parse for their documentation. - def JSON(object, opts = {}) + def JSON(object, *args) if object.respond_to? :to_str - JSON.parse(object.to_str, opts) + JSON.parse(object.to_str, args.first) else - JSON.generate(object, opts) + JSON.generate(object, args.first) end end end diff --git a/ext/json/lib/json/editor.rb b/ext/json/lib/json/editor.rb deleted file mode 100644 index 1e13f33c8c..0000000000 --- a/ext/json/lib/json/editor.rb +++ /dev/null @@ -1,1371 +0,0 @@ -# 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/ext/json/lib/json/ext.rb b/ext/json/lib/json/ext.rb index 719e56025c..a5e3148c57 100644 --- a/ext/json/lib/json/ext.rb +++ b/ext/json/lib/json/ext.rb @@ -6,10 +6,10 @@ module JSON module Ext require 'json/ext/parser' require 'json/ext/generator' - $DEBUG and warn "Using c extension for JSON." + $DEBUG and warn "Using Ext extension for JSON." JSON.parser = Parser JSON.generator = Generator end - JSON_LOADED = true + JSON_LOADED = true unless const_defined?(:JSON_LOADED) end diff --git a/ext/json/lib/json/version.rb b/ext/json/lib/json/version.rb index de7690b57c..beff08b1c7 100644 --- a/ext/json/lib/json/version.rb +++ b/ext/json/lib/json/version.rb @@ -1,6 +1,6 @@ module JSON # JSON version - VERSION = '1.4.2' + VERSION = '1.5.0' VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc: VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc: VERSION_MINOR = VERSION_ARRAY[1] # :nodoc: -- cgit v1.2.3