json_pure 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,17 +2,14 @@
2
2
 
3
3
  #include "ruby.h"
4
4
  #include "re.h"
5
+ #include "st.h"
5
6
  #include "unicode.h"
6
7
 
7
- #ifndef swap16
8
- #define swap16(x) ((((x)&0xFF)<<8) | (((x)>>8)&0xFF))
9
- #endif
10
-
11
8
  #define EVIL 0x666
12
9
 
13
- static VALUE mJSON, mExt, cParser, eParserError;
10
+ static VALUE mJSON, mExt, cParser, eParserError, eNestingError;
14
11
 
15
- static ID i_json_creatable_p, i_json_create, i_create_id, i_chr;
12
+ static ID i_json_creatable_p, i_json_create, i_create_id, i_chr, i_max_nesting;
16
13
 
17
14
  typedef struct JSON_ParserStruct {
18
15
  VALUE Vsource;
@@ -20,6 +17,8 @@ typedef struct JSON_ParserStruct {
20
17
  long len;
21
18
  char *memo;
22
19
  VALUE create_id;
20
+ int max_nesting;
21
+ int current_nesting;
23
22
  } JSON_Parser;
24
23
 
25
24
  static char *JSON_parse_object(JSON_Parser *json, char *p, char *pe, VALUE *result);
@@ -95,6 +94,11 @@ static char *JSON_parse_object(JSON_Parser *json, char *p, char *pe, VALUE *resu
95
94
  {
96
95
  int cs = EVIL;
97
96
  VALUE last_name = Qnil;
97
+
98
+ if (json->max_nesting && json->current_nesting > json->max_nesting) {
99
+ rb_raise(eNestingError, "nesting of %d is to deep", json->current_nesting);
100
+ }
101
+
98
102
  *result = rb_hash_new();
99
103
 
100
104
  %% write init;
@@ -144,12 +148,18 @@ static char *JSON_parse_object(JSON_Parser *json, char *p, char *pe, VALUE *resu
144
148
  }
145
149
 
146
150
  action parse_array {
147
- char *np = JSON_parse_array(json, fpc, pe, result);
151
+ char *np;
152
+ json->current_nesting += 1;
153
+ np = JSON_parse_array(json, fpc, pe, result);
154
+ json->current_nesting -= 1;
148
155
  if (np == NULL) fbreak; else fexec np;
149
156
  }
150
157
 
151
158
  action parse_object {
152
- char *np = JSON_parse_object(json, fpc, pe, result);
159
+ char *np;
160
+ json->current_nesting += 1;
161
+ np = JSON_parse_object(json, fpc, pe, result);
162
+ json->current_nesting -= 1;
153
163
  if (np == NULL) fbreak; else fexec np;
154
164
  }
155
165
 
@@ -269,6 +279,10 @@ static char *JSON_parse_float(JSON_Parser *json, char *p, char *pe, VALUE *resul
269
279
  static char *JSON_parse_array(JSON_Parser *json, char *p, char *pe, VALUE *result)
270
280
  {
271
281
  int cs = EVIL;
282
+
283
+ if (json->max_nesting && json->current_nesting > json->max_nesting) {
284
+ rb_raise(eNestingError, "nesting of %d is to deep", json->current_nesting);
285
+ }
272
286
  *result = rb_ary_new();
273
287
 
274
288
  %% write init;
@@ -281,7 +295,7 @@ static char *JSON_parse_array(JSON_Parser *json, char *p, char *pe, VALUE *resul
281
295
  }
282
296
  }
283
297
 
284
- static VALUE json_string_escape(char *p, char *pe)
298
+ static VALUE json_string_unescape(char *p, char *pe)
285
299
  {
286
300
  VALUE result = rb_str_buf_new(pe - p + 1);
287
301
 
@@ -322,6 +336,10 @@ static VALUE json_string_escape(char *p, char *pe)
322
336
  p = JSON_convert_UTF16_to_UTF8(result, p, pe, strictConversion);
323
337
  }
324
338
  break;
339
+ default:
340
+ rb_str_buf_cat(result, p, 1);
341
+ p++;
342
+ break;
325
343
  }
326
344
  } else {
327
345
  char *q = p;
@@ -340,13 +358,13 @@ static VALUE json_string_escape(char *p, char *pe)
340
358
  write data;
341
359
 
342
360
  action parse_string {
343
- *result = json_string_escape(json->memo + 1, p);
361
+ *result = json_string_unescape(json->memo + 1, p);
344
362
  if (NIL_P(*result)) fbreak; else fexec p + 1;
345
363
  }
346
364
 
347
365
  action exit { fbreak; }
348
366
 
349
- main := '"' ((^(["\\] | 0..0x1f) | '\\'["\\/bfnrt] | '\\u'[0-9a-fA-F]{4})* %parse_string) '"' @exit;
367
+ main := '"' ((^(["\\] | 0..0x1f) | '\\'["\\/bfnrt] | '\\u'[0-9a-fA-F]{4} | '\\'^(["\\/bfnrtu]|0..0x1f))* %parse_string) '"' @exit;
350
368
  }%%
351
369
 
352
370
  static char *JSON_parse_string(JSON_Parser *json, char *p, char *pe, VALUE *result)
@@ -374,12 +392,16 @@ static char *JSON_parse_string(JSON_Parser *json, char *p, char *pe, VALUE *resu
374
392
  include JSON_common;
375
393
 
376
394
  action parse_object {
377
- char *np = JSON_parse_object(json, fpc, pe, &result);
395
+ char *np;
396
+ json->current_nesting = 1;
397
+ np = JSON_parse_object(json, fpc, pe, &result);
378
398
  if (np == NULL) fbreak; else fexec np;
379
399
  }
380
400
 
381
401
  action parse_array {
382
- char *np = JSON_parse_array(json, fpc, pe, &result);
402
+ char *np;
403
+ json->current_nesting = 1;
404
+ np = JSON_parse_array(json, fpc, pe, &result);
383
405
  if (np == NULL) fbreak; else fexec np;
384
406
  }
385
407
 
@@ -402,21 +424,51 @@ static char *JSON_parse_string(JSON_Parser *json, char *p, char *pe, VALUE *resu
402
424
  */
403
425
 
404
426
  /*
405
- * call-seq: new(source)
427
+ * call-seq: new(source, opts => {})
406
428
  *
407
429
  * Creates a new JSON::Ext::Parser instance for the string _source_.
430
+ *
431
+ * Creates a new JSON::Ext::Parser instance for the string _source_.
432
+ *
433
+ * It will be configured by the _opts_ hash. _opts_ can have the following
434
+ * keys:
435
+ *
436
+ * _opts_ can have the following keys:
437
+ * * *max_nesting*: The maximum depth of nesting allowed in the parsed data
438
+ * structures. Disable depth checking with :max_nesting => false.
408
439
  */
409
- static VALUE cParser_initialize(VALUE self, VALUE source)
440
+ static VALUE cParser_initialize(int argc, VALUE *argv, VALUE self)
410
441
  {
411
442
  char *ptr;
412
443
  long len;
444
+ VALUE source, opts;
413
445
  GET_STRUCT;
446
+ rb_scan_args(argc, argv, "11", &source, &opts);
414
447
  source = StringValue(source);
415
448
  ptr = RSTRING_PTR(source);
416
449
  len = RSTRING_LEN(source);
417
450
  if (len < 2) {
418
451
  rb_raise(eParserError, "A JSON text must at least contain two octets!");
419
452
  }
453
+ json->max_nesting = 19;
454
+ if (!NIL_P(opts)) {
455
+ opts = rb_convert_type(opts, T_HASH, "Hash", "to_hash");
456
+ if (NIL_P(opts)) {
457
+ rb_raise(rb_eArgError, "opts needs to be like a hash");
458
+ } else {
459
+ VALUE s_max_nesting = ID2SYM(i_max_nesting);
460
+ if (st_lookup(RHASH(opts)->tbl, s_max_nesting, 0)) {
461
+ VALUE max_nesting = rb_hash_aref(opts, s_max_nesting);
462
+ if (RTEST(max_nesting)) {
463
+ Check_Type(max_nesting, T_FIXNUM);
464
+ json->max_nesting = FIX2INT(max_nesting);
465
+ } else {
466
+ json->max_nesting = 0;
467
+ }
468
+ }
469
+ }
470
+ }
471
+ json->current_nesting = 0;
420
472
  /*
421
473
  Convert these?
422
474
  if (len >= 4 && ptr[0] == 0 && ptr[1] == 0 && ptr[2] == 0) {
@@ -503,8 +555,9 @@ void Init_parser()
503
555
  mExt = rb_define_module_under(mJSON, "Ext");
504
556
  cParser = rb_define_class_under(mExt, "Parser", rb_cObject);
505
557
  eParserError = rb_path2class("JSON::ParserError");
558
+ eNestingError = rb_path2class("JSON::NestingError");
506
559
  rb_define_alloc_func(cParser, cJSON_parser_s_allocate);
507
- rb_define_method(cParser, "initialize", cParser_initialize, 1);
560
+ rb_define_method(cParser, "initialize", cParser_initialize, -1);
508
561
  rb_define_method(cParser, "parse", cParser_parse, 0);
509
562
  rb_define_method(cParser, "source", cParser_source, 0);
510
563
 
@@ -512,4 +565,5 @@ void Init_parser()
512
565
  i_json_create = rb_intern("json_create");
513
566
  i_create_id = rb_intern("create_id");
514
567
  i_chr = rb_intern("chr");
568
+ i_max_nesting = rb_intern("max_nesting");
515
569
  }
data/lib/json.rb CHANGED
@@ -34,9 +34,8 @@ require 'json/common'
34
34
  #
35
35
  # == License
36
36
  #
37
- # This is free software; you can redistribute it and/or modify it under the
38
- # terms of the GNU General Public License Version 2 as published by the Free
39
- # Software Foundation: www.gnu.org/copyleft/gpl.html
37
+ # This software is distributed under the same license as Ruby itself, see
38
+ # http://www.ruby-lang.org/en/LICENSE.txt.
40
39
  #
41
40
  # == Download
42
41
  #
data/lib/json/common.rb CHANGED
@@ -77,6 +77,10 @@ module JSON
77
77
  # This exception is raised, if a parser error occurs.
78
78
  class ParserError < JSONError; end
79
79
 
80
+ # This exception is raised, if the nesting of parsed datastructures is too
81
+ # deep.
82
+ class NestingError < ParserError; end
83
+
80
84
  # This exception is raised, if a generator or unparser error occurs.
81
85
  class GeneratorError < JSONError; end
82
86
  # For backwards compatibility
@@ -93,8 +97,13 @@ module JSON
93
97
  module_function
94
98
 
95
99
  # Parse the JSON string _source_ into a Ruby data structure and return it.
96
- def parse(source)
97
- JSON.parser.new(source).parse
100
+ #
101
+ # _opts_ can have the following
102
+ # keys:
103
+ # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
104
+ # structures. Disable depth checking with :max_nesting => false.
105
+ def parse(source, opts = {})
106
+ JSON.parser.new(source, opts).parse
98
107
  end
99
108
 
100
109
  # Unparse the Ruby data structure _obj_ into a single line JSON string and
data/lib/json/editor.rb CHANGED
@@ -1,1207 +1,1295 @@
1
- # To use the GUI JSON editor, start the edit_json.rb executable script. It
2
- # requires ruby-gtk to be installed.
3
-
4
- require 'gtk2'
5
- require 'iconv'
6
- require 'json'
7
- require 'rbconfig'
8
-
9
- module JSON
10
- module Editor
11
- include Gtk
12
-
13
- # Beginning of the editor window title
14
- TITLE = 'JSON Editor'.freeze
15
-
16
- # Columns constants
17
- ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2
18
-
19
- # JSON primitive types (Containers)
20
- CONTAINER_TYPES = %w[Array Hash].sort
21
- # All JSON primitive types
22
- ALL_TYPES = (%w[TrueClass FalseClass Numeric String NilClass] +
23
- CONTAINER_TYPES).sort
24
-
25
- # The Nodes necessary for the tree representation of a JSON document
26
- ALL_NODES = (ALL_TYPES + %w[Key]).sort
27
-
28
- # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
29
- def Editor.fetch_icon(name)
30
- @icon_cache ||= {}
31
- unless @icon_cache.key?(name)
32
- path = File.dirname(__FILE__)
33
- @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
34
- end
35
- @icon_cache[name]
36
- end
37
-
38
- # Opens an error dialog on top of _window_ showing the error message
39
- # _text_.
40
- def Editor.error_dialog(window, text)
41
- dialog = MessageDialog.new(window, Dialog::MODAL,
42
- MessageDialog::ERROR,
43
- MessageDialog::BUTTONS_CLOSE, text)
44
- dialog.run
45
- rescue TypeError
46
- dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
47
- MessageDialog::ERROR,
48
- MessageDialog::BUTTONS_CLOSE, text)
49
- dialog.run
50
- ensure
51
- dialog.destroy if dialog
52
- end
53
-
54
- # Opens a yes/no question dialog on top of _window_ showing the error
55
- # message _text_. If yes was answered _true_ is returned, otherwise
56
- # _false_.
57
- def Editor.question_dialog(window, text)
58
- dialog = MessageDialog.new(window, Dialog::MODAL,
59
- MessageDialog::QUESTION,
60
- MessageDialog::BUTTONS_YES_NO, text)
61
- dialog.run do |response|
62
- return Gtk::Dialog::RESPONSE_YES === response
63
- end
64
- ensure
65
- dialog.destroy if dialog
66
- end
67
-
68
- # Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
69
- # data structure and return it.
70
- def Editor.model2data(iter)
71
- case iter.type
72
- when 'Hash'
73
- hash = {}
74
- iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
75
- hash
76
- when 'Array'
77
- array = Array.new(iter.n_children)
78
- iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
79
- array
80
- when 'Key'
81
- iter.content
82
- when 'String'
83
- iter.content
84
- when 'Numeric'
85
- content = iter.content
86
- if /\./.match(content)
87
- content.to_f
88
- else
89
- content.to_i
90
- end
91
- when 'TrueClass'
92
- true
93
- when 'FalseClass'
94
- false
95
- when 'NilClass'
96
- nil
97
- else
98
- fail "Unknown type found in model: #{iter.type}"
99
- end
100
- end
101
-
102
- # Convert the Ruby data structure _data_ into tree model data for Gtk and
103
- # returns the whole model. If the parameter _model_ wasn't given a new
104
- # Gtk::TreeStore is created as the model. The _parent_ parameter specifies
105
- # the parent node (iter, Gtk:TreeIter instance) to which the data is
106
- # appended, alternativeley the result of the yielded block is used as iter.
107
- def Editor.data2model(data, model = nil, parent = nil)
108
- model ||= TreeStore.new(Gdk::Pixbuf, String, String)
109
- iter = if block_given?
110
- yield model
111
- else
112
- model.append(parent)
113
- end
114
- case data
115
- when Hash
116
- iter.type = 'Hash'
117
- data.sort.each do |key, value|
118
- pair_iter = model.append(iter)
119
- pair_iter.type = 'Key'
120
- pair_iter.content = key.to_s
121
- Editor.data2model(value, model, pair_iter)
122
- end
123
- when Array
124
- iter.type = 'Array'
125
- data.each do |value|
126
- Editor.data2model(value, model, iter)
127
- end
128
- when Numeric
129
- iter.type = 'Numeric'
130
- iter.content = data.to_s
131
- when String, true, false, nil
132
- iter.type = data.class.name
133
- iter.content = data.nil? ? 'null' : data.to_s
134
- else
135
- iter.type = 'String'
136
- iter.content = data.to_s
137
- end
138
- model
139
- end
140
-
141
- # The Gtk::TreeIter class is reopened and some auxiliary methods are added.
142
- class Gtk::TreeIter
143
- include Enumerable
144
-
145
- # Traverse each of this Gtk::TreeIter instance's children
146
- # and yield to them.
147
- def each
148
- n_children.times { |i| yield nth_child(i) }
149
- end
150
-
151
- # Recursively traverse all nodes of this Gtk::TreeIter's subtree
152
- # (including self) and yield to them.
153
- def recursive_each(&block)
154
- yield self
155
- each do |i|
156
- i.recursive_each(&block)
157
- end
158
- end
159
-
160
- # Remove the subtree of this Gtk::TreeIter instance from the
161
- # model _model_.
162
- def remove_subtree(model)
163
- while current = first_child
164
- model.remove(current)
165
- end
166
- end
167
-
168
- # Returns the type of this node.
169
- def type
170
- self[TYPE_COL]
171
- end
172
-
173
- # Sets the type of this node to _value_. This implies setting
174
- # the respective icon accordingly.
175
- def type=(value)
176
- self[TYPE_COL] = value
177
- self[ICON_COL] = Editor.fetch_icon(value)
178
- end
179
-
180
- # Returns the content of this node.
181
- def content
182
- self[CONTENT_COL]
183
- end
184
-
185
- # Sets the content of this node to _value_.
186
- def content=(value)
187
- self[CONTENT_COL] = value
188
- end
189
- end
190
-
191
- # This module bundles some method, that can be used to create a menu. It
192
- # should be included into the class in question.
193
- module MenuExtension
194
- include Gtk
195
-
196
- # Creates a Menu, that includes MenuExtension. _treeview_ is the
197
- # Gtk::TreeView, on which it operates.
198
- def initialize(treeview)
199
- @treeview = treeview
200
- @menu = Menu.new
201
- end
202
-
203
- # Returns the Gtk::TreeView of this menu.
204
- attr_reader :treeview
205
-
206
- # Returns the menu.
207
- attr_reader :menu
208
-
209
- # Adds a Gtk::SeparatorMenuItem to this instance's #menu.
210
- def add_separator
211
- menu.append SeparatorMenuItem.new
212
- end
213
-
214
- # Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
215
- # string, _klass_ is the item type, and _callback_ is the procedure, that
216
- # is called if the _item_ is activated.
217
- def add_item(label, klass = MenuItem, &callback)
218
- item = klass.new(label)
219
- item.signal_connect(:activate, &callback)
220
- menu.append item
221
- item
222
- end
223
-
224
- # This method should be implemented in subclasses to create the #menu of
225
- # this instance. It has to be called after an instance of this class is
226
- # created, to build the menu.
227
- def create
228
- raise NotImplementedError
229
- end
230
-
231
- def method_missing(*a, &b)
232
- treeview.__send__(*a, &b)
233
- end
234
- end
235
-
236
- # This class creates the popup menu, that opens when clicking onto the
237
- # treeview.
238
- class PopUpMenu
239
- include MenuExtension
240
-
241
- # Change the type or content of the selected node.
242
- def change_node(item)
243
- if current = selection.selected
244
- parent = current.parent
245
- old_type, old_content = current.type, current.content
246
- if ALL_TYPES.include?(old_type)
247
- @clipboard_data = Editor.model2data(current)
248
- type, content = ask_for_element(parent, current.type,
249
- current.content)
250
- if type
251
- current.type, current.content = type, content
252
- current.remove_subtree(model)
253
- toplevel.display_status("Changed a node in tree.")
254
- window.change
255
- end
256
- else
257
- toplevel.display_status(
258
- "Cannot change node of type #{old_type} in tree!")
259
- end
260
- end
261
- end
262
-
263
- # Cut the selected node and its subtree, and save it into the
264
- # clipboard.
265
- def cut_node(item)
266
- if current = selection.selected
267
- if current and current.type == 'Key'
268
- @clipboard_data = {
269
- current.content => Editor.model2data(current.first_child)
270
- }
271
- else
272
- @clipboard_data = Editor.model2data(current)
273
- end
274
- model.remove(current)
275
- window.change
276
- toplevel.display_status("Cut a node from tree.")
277
- end
278
- end
279
-
280
- # Copy the selected node and its subtree, and save it into the
281
- # clipboard.
282
- def copy_node(item)
283
- if current = selection.selected
284
- if current and current.type == 'Key'
285
- @clipboard_data = {
286
- current.content => Editor.model2data(current.first_child)
287
- }
288
- else
289
- @clipboard_data = Editor.model2data(current)
290
- end
291
- window.change
292
- toplevel.display_status("Copied a node from tree.")
293
- end
294
- end
295
-
296
- # Paste the data in the clipboard into the selected Array or Hash by
297
- # appending it.
298
- def paste_node_appending(item)
299
- if current = selection.selected
300
- if @clipboard_data
301
- case current.type
302
- when 'Array'
303
- Editor.data2model(@clipboard_data, model, current)
304
- expand_collapse(current)
305
- when 'Hash'
306
- if @clipboard_data.is_a? Hash
307
- parent = current.parent
308
- hash = Editor.model2data(current)
309
- model.remove(current)
310
- hash.update(@clipboard_data)
311
- Editor.data2model(hash, model, parent)
312
- if parent
313
- expand_collapse(parent)
314
- elsif @expanded
315
- expand_all
316
- end
317
- window.change
318
- else
319
- toplevel.display_status(
320
- "Cannot paste non-#{current.type} data into '#{current.type}'!")
321
- end
322
- else
323
- toplevel.display_status(
324
- "Cannot paste node below '#{current.type}'!")
325
- end
326
- else
327
- toplevel.display_status("Nothing to paste in clipboard!")
328
- end
329
- else
330
- toplevel.display_status("Append a node into the root first!")
331
- end
332
- end
333
-
334
- # Paste the data in the clipboard into the selected Array inserting it
335
- # before the selected element.
336
- def paste_node_inserting_before(item)
337
- if current = selection.selected
338
- if @clipboard_data
339
- parent = current.parent or return
340
- parent_type = parent.type
341
- if parent_type == 'Array'
342
- selected_index = parent.each_with_index do |c, i|
343
- break i if c == current
344
- end
345
- Editor.data2model(@clipboard_data, model, parent) do |m|
346
- m.insert_before(parent, current)
347
- end
348
- expand_collapse(current)
349
- toplevel.display_status("Inserted an element to " +
350
- "'#{parent_type}' before index #{selected_index}.")
351
- window.change
352
- else
353
- toplevel.display_status(
354
- "Cannot insert node below '#{parent_type}'!")
355
- end
356
- else
357
- toplevel.display_status("Nothing to paste in clipboard!")
358
- end
359
- else
360
- toplevel.display_status("Append a node into the root first!")
361
- end
362
- end
363
-
364
- # Append a new node to the selected Hash or Array.
365
- def append_new_node(item)
366
- if parent = selection.selected
367
- parent_type = parent.type
368
- case parent_type
369
- when 'Hash'
370
- key, type, content = ask_for_hash_pair(parent)
371
- key or return
372
- iter = create_node(parent, 'Key', key)
373
- iter = create_node(iter, type, content)
374
- toplevel.display_status(
375
- "Added a (key, value)-pair to '#{parent_type}'.")
376
- window.change
377
- when 'Array'
378
- type, content = ask_for_element(parent)
379
- type or return
380
- iter = create_node(parent, type, content)
381
- window.change
382
- toplevel.display_status("Appendend an element to '#{parent_type}'.")
383
- else
384
- toplevel.display_status("Cannot append to '#{parent_type}'!")
385
- end
386
- else
387
- type, content = ask_for_element
388
- type or return
389
- iter = create_node(nil, type, content)
390
- window.change
391
- end
392
- end
393
-
394
- # Insert a new node into an Array before the selected element.
395
- def insert_new_node(item)
396
- if current = selection.selected
397
- parent = current.parent or return
398
- parent_parent = parent.parent
399
- parent_type = parent.type
400
- if parent_type == 'Array'
401
- selected_index = parent.each_with_index do |c, i|
402
- break i if c == current
403
- end
404
- type, content = ask_for_element(parent)
405
- type or return
406
- iter = model.insert_before(parent, current)
407
- iter.type, iter.content = type, content
408
- toplevel.display_status("Inserted an element to " +
409
- "'#{parent_type}' before index #{selected_index}.")
410
- window.change
411
- else
412
- toplevel.display_status(
413
- "Cannot insert node below '#{parent_type}'!")
414
- end
415
- else
416
- toplevel.display_status("Append a node into the root first!")
417
- end
418
- end
419
-
420
- # Recursively collapse/expand a subtree starting from the selected node.
421
- def collapse_expand(item)
422
- if current = selection.selected
423
- if row_expanded?(current.path)
424
- collapse_row(current.path)
425
- else
426
- expand_row(current.path, true)
427
- end
428
- else
429
- toplevel.display_status("Append a node into the root first!")
430
- end
431
- end
432
-
433
- # Create the menu.
434
- def create
435
- add_item("Change node", &method(:change_node))
436
- add_separator
437
- add_item("Cut node", &method(:cut_node))
438
- add_item("Copy node", &method(:copy_node))
439
- add_item("Paste node (appending)", &method(:paste_node_appending))
440
- add_item("Paste node (inserting before)",
441
- &method(:paste_node_inserting_before))
442
- add_separator
443
- add_item("Append new node", &method(:append_new_node))
444
- add_item("Insert new node before", &method(:insert_new_node))
445
- add_separator
446
- add_item("Collapse/Expand node (recursively)",
447
- &method(:collapse_expand))
448
-
449
- menu.show_all
450
- signal_connect(:button_press_event) do |widget, event|
451
- if event.kind_of? Gdk::EventButton and event.button == 3
452
- menu.popup(nil, nil, event.button, event.time)
453
- end
454
- end
455
- signal_connect(:popup_menu) do
456
- menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
457
- end
458
- end
459
- end
460
-
461
- # This class creates the File pulldown menu.
462
- class FileMenu
463
- include MenuExtension
464
-
465
- # Clear the model and filename, but ask to save the JSON document, if
466
- # unsaved changes have occured.
467
- def new(item)
468
- window.clear
469
- end
470
-
471
- # Open a file and load it into the editor. Ask to save the JSON document
472
- # first, if unsaved changes have occured.
473
- def open(item)
474
- window.file_open
475
- end
476
-
477
- # Revert the current JSON document in the editor to the saved version.
478
- def revert(item)
479
- window.instance_eval do
480
- @filename and file_open(@filename)
481
- end
482
- end
483
-
484
- # Save the current JSON document.
485
- def save(item)
486
- window.file_save
487
- end
488
-
489
- # Save the current JSON document under the given filename.
490
- def save_as(item)
491
- window.file_save_as
492
- end
493
-
494
- # Quit the editor, after asking to save any unsaved changes first.
495
- def quit(item)
496
- window.quit
497
- end
498
-
499
- # Create the menu.
500
- def create
501
- title = MenuItem.new('File')
502
- title.submenu = menu
503
- add_item('New', &method(:new))
504
- add_item('Open', &method(:open))
505
- add_item('Revert', &method(:revert))
506
- add_separator
507
- add_item('Save', &method(:save))
508
- add_item('Save As', &method(:save_as))
509
- add_separator
510
- add_item('Quit', &method(:quit))
511
- title
512
- end
513
- end
514
-
515
- # This class creates the Edit pulldown menu.
516
- class EditMenu
517
- include MenuExtension
518
-
519
- # Find a string in all nodes' contents and select the found node in the
520
- # treeview.
521
- def find(item)
522
- search = ask_for_find_term or return
523
- begin
524
- @search = Regexp.new(search)
525
- rescue => e
526
- Editor.error_dialog(self, "Evaluation of regex /#{search}/ failed: #{e}!")
527
- return
528
- end
529
- iter = model.get_iter('0')
530
- iter.recursive_each do |i|
531
- if @iter
532
- if @iter != i
533
- next
534
- else
535
- @iter = nil
536
- next
537
- end
538
- elsif @search.match(i[CONTENT_COL])
539
- set_cursor(i.path, nil, false)
540
- @iter = i
541
- break
542
- end
543
- end
544
- end
545
-
546
- # Repeat the last search given by #find.
547
- def find_again(item)
548
- @search or return
549
- iter = model.get_iter('0')
550
- iter.recursive_each do |i|
551
- if @iter
552
- if @iter != i
553
- next
554
- else
555
- @iter = nil
556
- next
557
- end
558
- elsif @search.match(i[CONTENT_COL])
559
- set_cursor(i.path, nil, false)
560
- @iter = i
561
- break
562
- end
563
- end
564
- end
565
-
566
- # Sort (Reverse sort) all elements of the selected array by the given
567
- # expression. _x_ is the element in question.
568
- def sort(item)
569
- if current = selection.selected
570
- if current.type == 'Array'
571
- parent = current.parent
572
- ary = Editor.model2data(current)
573
- order, reverse = ask_for_order
574
- order or return
575
- begin
576
- block = eval "lambda { |x| #{order} }"
577
- if reverse
578
- ary.sort! { |a,b| block[b] <=> block[a] }
579
- else
580
- ary.sort! { |a,b| block[a] <=> block[b] }
581
- end
582
- rescue => e
583
- Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
584
- else
585
- Editor.data2model(ary, model, parent) do |m|
586
- m.insert_before(parent, current)
587
- end
588
- model.remove(current)
589
- expand_collapse(parent)
590
- window.change
591
- toplevel.display_status("Array has been sorted.")
592
- end
593
- else
594
- toplevel.display_status("Only Array nodes can be sorted!")
595
- end
596
- else
597
- toplevel.display_status("Select an Array to sort first!")
598
- end
599
- end
600
-
601
- # Create the menu.
602
- def create
603
- title = MenuItem.new('Edit')
604
- title.submenu = menu
605
- add_item('Find', &method(:find))
606
- add_item('Find Again', &method(:find_again))
607
- add_separator
608
- add_item('Sort', &method(:sort))
609
- title
610
- end
611
- end
612
-
613
- class OptionsMenu
614
- include MenuExtension
615
-
616
- # Collapse/Expand all nodes by default.
617
- def collapsed_nodes(item)
618
- if expanded
619
- self.expanded = false
620
- collapse_all
621
- else
622
- self.expanded = true
623
- expand_all
624
- end
625
- end
626
-
627
- # Toggle pretty saving mode on/off.
628
- def pretty_saving(item)
629
- @pretty_item.toggled
630
- window.change
631
- end
632
-
633
- attr_reader :pretty_item
634
-
635
- # Create the menu.
636
- def create
637
- title = MenuItem.new('Options')
638
- title.submenu = menu
639
- add_item('Collapsed nodes', CheckMenuItem, &method(:collapsed_nodes))
640
- @pretty_item = add_item('Pretty saving', CheckMenuItem,
641
- &method(:pretty_saving))
642
- @pretty_item.active = true
643
- window.unchange
644
- title
645
- end
646
- end
647
-
648
- # This class inherits from Gtk::TreeView, to configure it and to add a lot
649
- # of behaviour to it.
650
- class JSONTreeView < Gtk::TreeView
651
- include Gtk
652
-
653
- # Creates a JSONTreeView instance, the parameter _window_ is
654
- # a MainWindow instance and used for self delegation.
655
- def initialize(window)
656
- @window = window
657
- super(TreeStore.new(Gdk::Pixbuf, String, String))
658
- self.selection.mode = SELECTION_BROWSE
659
-
660
- @expanded = false
661
- self.headers_visible = false
662
- add_columns
663
- add_popup_menu
664
- end
665
-
666
- # Returns the MainWindow instance of this JSONTreeView.
667
- attr_reader :window
668
-
669
- # Returns true, if nodes are autoexpanding, false otherwise.
670
- attr_accessor :expanded
671
-
672
- private
673
-
674
- def add_columns
675
- cell = CellRendererPixbuf.new
676
- column = TreeViewColumn.new('Icon', cell,
677
- 'pixbuf' => ICON_COL
678
- )
679
- append_column(column)
680
-
681
- cell = CellRendererText.new
682
- column = TreeViewColumn.new('Type', cell,
683
- 'text' => TYPE_COL
684
- )
685
- append_column(column)
686
-
687
- cell = CellRendererText.new
688
- cell.editable = true
689
- column = TreeViewColumn.new('Content', cell,
690
- 'text' => CONTENT_COL
691
- )
692
- cell.signal_connect(:edited, &method(:cell_edited))
693
- append_column(column)
694
- end
695
-
696
- def unify_key(iter, key)
697
- return unless iter.type == 'Key'
698
- parent = iter.parent
699
- if parent.any? { |c| c != iter and c.content == key }
700
- old_key = key
701
- i = 0
702
- begin
703
- key = sprintf("%s.%d", old_key, i += 1)
704
- end while parent.any? { |c| c != iter and c.content == key }
705
- end
706
- iter.content = key
707
- end
708
-
709
- def cell_edited(cell, path, value)
710
- iter = model.get_iter(path)
711
- case iter.type
712
- when 'Key'
713
- unify_key(iter, value)
714
- toplevel.display_status('Key has been changed.')
715
- when 'FalseClass'
716
- value.downcase!
717
- if value == 'true'
718
- iter.type, iter.content = 'TrueClass', 'true'
719
- end
720
- when 'TrueClass'
721
- value.downcase!
722
- if value == 'false'
723
- iter.type, iter.content = 'FalseClass', 'false'
724
- end
725
- when 'Numeric'
726
- iter.content = (Integer(value) rescue Float(value) rescue 0).to_s
727
- when 'String'
728
- iter.content = value
729
- when 'Hash', 'Array'
730
- return
731
- else
732
- fail "Unknown type found in model: #{iter.type}"
733
- end
734
- window.change
735
- end
736
-
737
- def configure_value(value, type)
738
- value.editable = false
739
- case type
740
- when 'Array', 'Hash'
741
- value.text = ''
742
- when 'TrueClass'
743
- value.text = 'true'
744
- when 'FalseClass'
745
- value.text = 'false'
746
- when 'NilClass'
747
- value.text = 'null'
748
- when 'Numeric', 'String'
749
- value.text ||= ''
750
- value.editable = true
751
- else
752
- raise ArgumentError, "unknown type '#{type}' encountered"
753
- end
754
- end
755
-
756
- def add_popup_menu
757
- menu = PopUpMenu.new(self)
758
- menu.create
759
- end
760
-
761
- public
762
-
763
- # Create a _type_ node with content _content_, and add it to _parent_
764
- # in the model. If _parent_ is nil, create a new model and put it into
765
- # the editor treeview.
766
- def create_node(parent, type, content)
767
- iter = if parent
768
- model.append(parent)
769
- else
770
- new_model = Editor.data2model(nil)
771
- toplevel.view_new_model(new_model)
772
- new_model.iter_first
773
- end
774
- iter.type, iter.content = type, content
775
- expand_collapse(parent) if parent
776
- iter
777
- end
778
-
779
- # Ask for a hash key, value pair to be added to the Hash node _parent_.
780
- def ask_for_hash_pair(parent)
781
- key_input = type_input = value_input = nil
782
-
783
- dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
784
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
785
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
786
- )
787
-
788
- hbox = HBox.new(false, 5)
789
- hbox.pack_start(Label.new("Key:"))
790
- hbox.pack_start(key_input = Entry.new)
791
- key_input.text = @key || ''
792
- dialog.vbox.add(hbox)
793
- key_input.signal_connect(:activate) do
794
- if parent.any? { |c| c.content == key_input.text }
795
- toplevel.display_status('Key already exists in Hash!')
796
- key_input.text = ''
797
- else
798
- toplevel.display_status('Key has been changed.')
799
- end
800
- end
801
-
802
- hbox = HBox.new(false, 5)
803
- hbox.add(Label.new("Type:"))
804
- hbox.pack_start(type_input = ComboBox.new(true))
805
- ALL_TYPES.each { |t| type_input.append_text(t) }
806
- type_input.active = @type || 0
807
- dialog.vbox.add(hbox)
808
-
809
- type_input.signal_connect(:changed) do
810
- value_input.editable = false
811
- case ALL_TYPES[type_input.active]
812
- when 'Array', 'Hash'
813
- value_input.text = ''
814
- when 'TrueClass'
815
- value_input.text = 'true'
816
- when 'FalseClass'
817
- value_input.text = 'false'
818
- when 'NilClass'
819
- value_input.text = 'null'
820
- else
821
- value_input.text = ''
822
- value_input.editable = true
823
- end
824
- end
825
-
826
- hbox = HBox.new(false, 5)
827
- hbox.add(Label.new("Value:"))
828
- hbox.pack_start(value_input = Entry.new)
829
- value_input.text = @value || ''
830
- dialog.vbox.add(hbox)
831
-
832
- dialog.show_all
833
- dialog.run do |response|
834
- if response == Dialog::RESPONSE_ACCEPT
835
- @key = key_input.text
836
- type = ALL_TYPES[@type = type_input.active]
837
- content = value_input.text
838
- return @key, type, content
839
- end
840
- end
841
- return
842
- ensure
843
- dialog.destroy
844
- end
845
-
846
- # Ask for an element to be appended _parent_.
847
- def ask_for_element(parent = nil, default_type = nil, value_text = @content)
848
- type_input = value_input = nil
849
-
850
- dialog = Dialog.new(
851
- "New element into #{parent ? parent.type : 'root'}",
852
- nil, nil,
853
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
854
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
855
- )
856
- hbox = HBox.new(false, 5)
857
- hbox.add(Label.new("Type:"))
858
- hbox.pack_start(type_input = ComboBox.new(true))
859
- default_active = 0
860
- types = parent ? ALL_TYPES : CONTAINER_TYPES
861
- types.each_with_index do |t, i|
862
- type_input.append_text(t)
863
- if t == default_type
864
- default_active = i
865
- end
866
- end
867
- type_input.active = default_active
868
- dialog.vbox.add(hbox)
869
- type_input.signal_connect(:changed) do
870
- configure_value(value_input, types[type_input.active])
871
- end
872
-
873
- hbox = HBox.new(false, 5)
874
- hbox.add(Label.new("Value:"))
875
- hbox.pack_start(value_input = Entry.new)
876
- value_input.text = value_text if value_text
877
- configure_value(value_input, types[type_input.active])
878
-
879
- dialog.vbox.add(hbox)
880
-
881
- dialog.show_all
882
- dialog.run do |response|
883
- if response == Dialog::RESPONSE_ACCEPT
884
- type = types[type_input.active]
885
- @content = case type
886
- when 'Numeric'
887
- Integer(value_input.text) rescue Float(value_input.text) rescue 0
888
- else
889
- value_input.text
890
- end.to_s
891
- return type, @content
892
- end
893
- end
894
- return
895
- ensure
896
- dialog.destroy if dialog
897
- end
898
-
899
- # Ask for an order criteria for sorting, using _x_ for the element in
900
- # question. Returns the order criterium, and true/false for reverse
901
- # sorting.
902
- def ask_for_order
903
- dialog = Dialog.new(
904
- "Give an order criterium for 'x'.",
905
- nil, nil,
906
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
907
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
908
- )
909
- hbox = HBox.new(false, 5)
910
-
911
- hbox.add(Label.new("Order:"))
912
- hbox.pack_start(order_input = Entry.new)
913
- order_input.text = @order || 'x'
914
-
915
- hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'))
916
-
917
- dialog.vbox.add(hbox)
918
-
919
- dialog.show_all
920
- dialog.run do |response|
921
- if response == Dialog::RESPONSE_ACCEPT
922
- return @order = order_input.text, reverse_checkbox.active?
923
- end
924
- end
925
- return
926
- ensure
927
- dialog.destroy if dialog
928
- end
929
-
930
- # Ask for a find term to search for in the tree. Returns the term as a
931
- # string.
932
- def ask_for_find_term
933
- dialog = Dialog.new(
934
- "Find a node matching regex in tree.",
935
- nil, nil,
936
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
937
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
938
- )
939
- hbox = HBox.new(false, 5)
940
-
941
- hbox.add(Label.new("Regex:"))
942
- hbox.pack_start(regex_input = Entry.new)
943
- regex_input.text = @regex || ''
944
-
945
- dialog.vbox.add(hbox)
946
-
947
- dialog.show_all
948
- dialog.run do |response|
949
- if response == Dialog::RESPONSE_ACCEPT
950
- return @regex = regex_input.text
951
- end
952
- end
953
- return
954
- ensure
955
- dialog.destroy if dialog
956
- end
957
-
958
- # Expand or collapse row pointed to by _iter_ according
959
- # to the #expanded attribute.
960
- def expand_collapse(iter)
961
- if expanded
962
- expand_row(iter.path, true)
963
- else
964
- collapse_row(iter.path)
965
- end
966
- end
967
- end
968
-
969
- # The editor main window
970
- class MainWindow < Gtk::Window
971
- include Gtk
972
-
973
- def initialize(encoding)
974
- @changed = false
975
- @encoding = encoding
976
- super(TOPLEVEL)
977
- display_title
978
- set_default_size(800, 600)
979
- signal_connect(:delete_event) { quit }
980
-
981
- vbox = VBox.new(false, 0)
982
- add(vbox)
983
- #vbox.border_width = 0
984
-
985
- @treeview = JSONTreeView.new(self)
986
- @treeview.signal_connect(:'cursor-changed') do
987
- display_status('')
988
- end
989
-
990
- menu_bar = create_menu_bar
991
- vbox.pack_start(menu_bar, false, false, 0)
992
-
993
- sw = ScrolledWindow.new(nil, nil)
994
- sw.shadow_type = SHADOW_ETCHED_IN
995
- sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
996
- vbox.pack_start(sw, true, true, 0)
997
- sw.add(@treeview)
998
-
999
- @status_bar = Statusbar.new
1000
- vbox.pack_start(@status_bar, false, false, 0)
1001
-
1002
- @filename ||= nil
1003
- if @filename
1004
- data = read_data(@filename)
1005
- view_new_model Editor.data2model(data)
1006
- end
1007
- end
1008
-
1009
- # Creates the menu bar with the pulldown menus and returns it.
1010
- def create_menu_bar
1011
- menu_bar = MenuBar.new
1012
- @file_menu = FileMenu.new(@treeview)
1013
- menu_bar.append @file_menu.create
1014
- @edit_menu = EditMenu.new(@treeview)
1015
- menu_bar.append @edit_menu.create
1016
- @options_menu = OptionsMenu.new(@treeview)
1017
- menu_bar.append @options_menu.create
1018
- menu_bar
1019
- end
1020
-
1021
- # Sets editor status to changed, to indicate that the edited data
1022
- # containts unsaved changes.
1023
- def change
1024
- @changed = true
1025
- display_title
1026
- end
1027
-
1028
- # Sets editor status to unchanged, to indicate that the edited data
1029
- # doesn't containt unsaved changes.
1030
- def unchange
1031
- @changed = false
1032
- display_title
1033
- end
1034
-
1035
- # Puts a new model _model_ into the Gtk::TreeView to be edited.
1036
- def view_new_model(model)
1037
- @treeview.model = model
1038
- @treeview.expanded = true
1039
- @treeview.expand_all
1040
- unchange
1041
- end
1042
-
1043
- # Displays _text_ in the status bar.
1044
- def display_status(text)
1045
- @cid ||= nil
1046
- @status_bar.pop(@cid) if @cid
1047
- @cid = @status_bar.get_context_id('dummy')
1048
- @status_bar.push(@cid, text)
1049
- end
1050
-
1051
- # Opens a dialog, asking, if changes should be saved to a file.
1052
- def ask_save
1053
- if Editor.question_dialog(self,
1054
- "Unsaved changes to JSON model. Save?")
1055
- if @filename
1056
- file_save
1057
- else
1058
- file_save_as
1059
- end
1060
- end
1061
- end
1062
-
1063
- # Quit this editor, that is, leave this editor's main loop.
1064
- def quit
1065
- ask_save if @changed
1066
- destroy
1067
- Gtk.main_quit
1068
- true
1069
- end
1070
-
1071
- # Display the new title according to the editor's current state.
1072
- def display_title
1073
- title = TITLE.dup
1074
- title << ": #@filename" if @filename
1075
- title << " *" if @changed
1076
- self.title = title
1077
- end
1078
-
1079
- # Clear the current model, after asking to save all unsaved changes.
1080
- def clear
1081
- ask_save if @changed
1082
- @filename = nil
1083
- self.view_new_model nil
1084
- end
1085
-
1086
- # Open the file _filename_ or call the #select_file method to ask for a
1087
- # filename.
1088
- def file_open(filename = nil)
1089
- filename = select_file('Open as a JSON file') unless filename
1090
- data = load_file(filename) or return
1091
- view_new_model Editor.data2model(data)
1092
- end
1093
-
1094
- # Save the current file.
1095
- def file_save
1096
- if @filename
1097
- store_file(@filename)
1098
- else
1099
- file_save_as
1100
- end
1101
- end
1102
-
1103
- # Save the current file as the filename
1104
- def file_save_as
1105
- filename = select_file('Save as a JSON file')
1106
- store_file(filename)
1107
- end
1108
-
1109
- # Store the current JSON document to _path_.
1110
- def store_file(path)
1111
- if path
1112
- data = Editor.model2data(@treeview.model.iter_first)
1113
- File.open(path + '.tmp', 'wb') do |output|
1114
- if @options_menu.pretty_item.active?
1115
- output.puts JSON.pretty_generate(data)
1116
- else
1117
- output.write JSON.unparse(data)
1118
- end
1119
- end
1120
- File.rename path + '.tmp', path
1121
- @filename = path
1122
- toplevel.display_status("Saved data to '#@filename'.")
1123
- unchange
1124
- end
1125
- rescue SystemCallError => e
1126
- Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
1127
- end
1128
-
1129
- # Load the file named _filename_ into the editor as a JSON document.
1130
- def load_file(filename)
1131
- if filename
1132
- if File.directory?(filename)
1133
- Editor.error_dialog(self, "Try to select a JSON file!")
1134
- return
1135
- else
1136
- data = read_data(filename)
1137
- @filename = filename
1138
- toplevel.display_status("Loaded data from '#@filename'.")
1139
- display_title
1140
- return data
1141
- end
1142
- end
1143
- end
1144
-
1145
- def check_pretty_printed(json)
1146
- pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
1147
- @options_menu.pretty_item.active = pretty
1148
- end
1149
- private :check_pretty_printed
1150
-
1151
- # Read a JSON document from the file named _filename_, parse it into a
1152
- # ruby data structure, and return the data.
1153
- def read_data(filename)
1154
- json = File.read(filename)
1155
- check_pretty_printed(json)
1156
- if @encoding && !/^utf8$/i.match(@encoding)
1157
- iconverter = Iconv.new('utf8', @encoding)
1158
- json = iconverter.iconv(json)
1159
- end
1160
- JSON::parse(json)
1161
- rescue JSON::JSONError => e
1162
- Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
1163
- return
1164
- rescue SystemCallError => e
1165
- quit
1166
- end
1167
-
1168
- # Open a file selecton dialog, displaying _message_, and return the
1169
- # selected filename or nil, if no file was selected.
1170
- def select_file(message)
1171
- filename = nil
1172
- fs = FileSelection.new(message).set_modal(true).
1173
- set_filename(Dir.pwd + "/").set_transient_for(self)
1174
- fs.signal_connect(:destroy) { Gtk.main_quit }
1175
- fs.ok_button.signal_connect(:clicked) do
1176
- filename = fs.filename
1177
- fs.destroy
1178
- Gtk.main_quit
1179
- end
1180
- fs.cancel_button.signal_connect(:clicked) do
1181
- fs.destroy
1182
- Gtk.main_quit
1183
- end
1184
- fs.show_all
1185
- Gtk.main
1186
- filename
1187
- end
1188
- end
1189
-
1190
- class << self
1191
- # Starts a JSON Editor. If a block was given, it yields
1192
- # to the JSON::Editor::MainWindow instance.
1193
- def start(encoding = nil) # :yield: window
1194
- encoding ||= 'utf8'
1195
- Gtk.init
1196
- @window = Editor::MainWindow.new(encoding)
1197
- @window.icon_list = [ Editor.fetch_icon('json') ]
1198
- yield @window if block_given?
1199
- @window.show_all
1200
- Gtk.main
1201
- end
1202
-
1203
- attr_reader :window
1204
- end
1205
- end
1206
- end
1207
- # vim: set et sw=2 ts=2:
1
+ # To use the GUI JSON editor, start the edit_json.rb executable script. It
2
+ # requires ruby-gtk to be installed.
3
+
4
+ require 'gtk2'
5
+ require 'iconv'
6
+ require 'json'
7
+ require 'rbconfig'
8
+ require 'open-uri'
9
+
10
+ module JSON
11
+ module Editor
12
+ include Gtk
13
+
14
+ # Beginning of the editor window title
15
+ TITLE = 'JSON Editor'.freeze
16
+
17
+ # Columns constants
18
+ ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2
19
+
20
+ # JSON primitive types (Containers)
21
+ CONTAINER_TYPES = %w[Array Hash].sort
22
+ # All JSON primitive types
23
+ ALL_TYPES = (%w[TrueClass FalseClass Numeric String NilClass] +
24
+ CONTAINER_TYPES).sort
25
+
26
+ # The Nodes necessary for the tree representation of a JSON document
27
+ ALL_NODES = (ALL_TYPES + %w[Key]).sort
28
+
29
+ DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event|
30
+ case event.keyval
31
+ when Gdk::Keyval::GDK_Return
32
+ dialog.response Dialog::RESPONSE_ACCEPT
33
+ when Gdk::Keyval::GDK_Escape
34
+ dialog.response Dialog::RESPONSE_REJECT
35
+ end
36
+ end
37
+
38
+ # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
39
+ def Editor.fetch_icon(name)
40
+ @icon_cache ||= {}
41
+ unless @icon_cache.key?(name)
42
+ path = File.dirname(__FILE__)
43
+ @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
44
+ end
45
+ @icon_cache[name]
46
+ end
47
+
48
+ # Opens an error dialog on top of _window_ showing the error message
49
+ # _text_.
50
+ def Editor.error_dialog(window, text)
51
+ dialog = MessageDialog.new(window, Dialog::MODAL,
52
+ MessageDialog::ERROR,
53
+ MessageDialog::BUTTONS_CLOSE, text)
54
+ dialog.show_all
55
+ window.focus = dialog
56
+ dialog.run
57
+ rescue TypeError
58
+ dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
59
+ MessageDialog::ERROR,
60
+ MessageDialog::BUTTONS_CLOSE, text)
61
+ dialog.show_all
62
+ window.focus = dialog
63
+ dialog.run
64
+ ensure
65
+ dialog.destroy if dialog
66
+ end
67
+
68
+ # Opens a yes/no question dialog on top of _window_ showing the error
69
+ # message _text_. If yes was answered _true_ is returned, otherwise
70
+ # _false_.
71
+ def Editor.question_dialog(window, text)
72
+ dialog = MessageDialog.new(window, Dialog::MODAL,
73
+ MessageDialog::QUESTION,
74
+ MessageDialog::BUTTONS_YES_NO, text)
75
+ dialog.show_all
76
+ window.focus = dialog
77
+ dialog.run do |response|
78
+ return Gtk::Dialog::RESPONSE_YES === response
79
+ end
80
+ ensure
81
+ dialog.destroy if dialog
82
+ end
83
+
84
+ # Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
85
+ # data structure and return it.
86
+ def Editor.model2data(iter)
87
+ return nil if iter.nil?
88
+ case iter.type
89
+ when 'Hash'
90
+ hash = {}
91
+ iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
92
+ hash
93
+ when 'Array'
94
+ array = Array.new(iter.n_children)
95
+ iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
96
+ array
97
+ when 'Key'
98
+ iter.content
99
+ when 'String'
100
+ iter.content
101
+ when 'Numeric'
102
+ content = iter.content
103
+ if /\./.match(content)
104
+ content.to_f
105
+ else
106
+ content.to_i
107
+ end
108
+ when 'TrueClass'
109
+ true
110
+ when 'FalseClass'
111
+ false
112
+ when 'NilClass'
113
+ nil
114
+ else
115
+ fail "Unknown type found in model: #{iter.type}"
116
+ end
117
+ end
118
+
119
+ # Convert the Ruby data structure _data_ into tree model data for Gtk and
120
+ # returns the whole model. If the parameter _model_ wasn't given a new
121
+ # Gtk::TreeStore is created as the model. The _parent_ parameter specifies
122
+ # the parent node (iter, Gtk:TreeIter instance) to which the data is
123
+ # appended, alternativeley the result of the yielded block is used as iter.
124
+ def Editor.data2model(data, model = nil, parent = nil)
125
+ model ||= TreeStore.new(Gdk::Pixbuf, String, String)
126
+ iter = if block_given?
127
+ yield model
128
+ else
129
+ model.append(parent)
130
+ end
131
+ case data
132
+ when Hash
133
+ iter.type = 'Hash'
134
+ data.sort.each do |key, value|
135
+ pair_iter = model.append(iter)
136
+ pair_iter.type = 'Key'
137
+ pair_iter.content = key.to_s
138
+ Editor.data2model(value, model, pair_iter)
139
+ end
140
+ when Array
141
+ iter.type = 'Array'
142
+ data.each do |value|
143
+ Editor.data2model(value, model, iter)
144
+ end
145
+ when Numeric
146
+ iter.type = 'Numeric'
147
+ iter.content = data.to_s
148
+ when String, true, false, nil
149
+ iter.type = data.class.name
150
+ iter.content = data.nil? ? 'null' : data.to_s
151
+ else
152
+ iter.type = 'String'
153
+ iter.content = data.to_s
154
+ end
155
+ model
156
+ end
157
+
158
+ # The Gtk::TreeIter class is reopened and some auxiliary methods are added.
159
+ class Gtk::TreeIter
160
+ include Enumerable
161
+
162
+ # Traverse each of this Gtk::TreeIter instance's children
163
+ # and yield to them.
164
+ def each
165
+ n_children.times { |i| yield nth_child(i) }
166
+ end
167
+
168
+ # Recursively traverse all nodes of this Gtk::TreeIter's subtree
169
+ # (including self) and yield to them.
170
+ def recursive_each(&block)
171
+ yield self
172
+ each do |i|
173
+ i.recursive_each(&block)
174
+ end
175
+ end
176
+
177
+ # Remove the subtree of this Gtk::TreeIter instance from the
178
+ # model _model_.
179
+ def remove_subtree(model)
180
+ while current = first_child
181
+ model.remove(current)
182
+ end
183
+ end
184
+
185
+ # Returns the type of this node.
186
+ def type
187
+ self[TYPE_COL]
188
+ end
189
+
190
+ # Sets the type of this node to _value_. This implies setting
191
+ # the respective icon accordingly.
192
+ def type=(value)
193
+ self[TYPE_COL] = value
194
+ self[ICON_COL] = Editor.fetch_icon(value)
195
+ end
196
+
197
+ # Returns the content of this node.
198
+ def content
199
+ self[CONTENT_COL]
200
+ end
201
+
202
+ # Sets the content of this node to _value_.
203
+ def content=(value)
204
+ self[CONTENT_COL] = value
205
+ end
206
+ end
207
+
208
+ # This module bundles some method, that can be used to create a menu. It
209
+ # should be included into the class in question.
210
+ module MenuExtension
211
+ include Gtk
212
+
213
+ # Creates a Menu, that includes MenuExtension. _treeview_ is the
214
+ # Gtk::TreeView, on which it operates.
215
+ def initialize(treeview)
216
+ @treeview = treeview
217
+ @menu = Menu.new
218
+ end
219
+
220
+ # Returns the Gtk::TreeView of this menu.
221
+ attr_reader :treeview
222
+
223
+ # Returns the menu.
224
+ attr_reader :menu
225
+
226
+ # Adds a Gtk::SeparatorMenuItem to this instance's #menu.
227
+ def add_separator
228
+ menu.append SeparatorMenuItem.new
229
+ end
230
+
231
+ # Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
232
+ # string, _klass_ is the item type, and _callback_ is the procedure, that
233
+ # is called if the _item_ is activated.
234
+ def add_item(label, keyval = nil, klass = MenuItem, &callback)
235
+ label = "#{label} (C-#{keyval.chr})" if keyval
236
+ item = klass.new(label)
237
+ item.signal_connect(:activate, &callback)
238
+ if keyval
239
+ self.signal_connect(:'key-press-event') do |item, event|
240
+ if event.state & Gdk::Window::ModifierType::CONTROL_MASK != 0 and
241
+ event.keyval == keyval
242
+ callback.call item
243
+ end
244
+ end
245
+ end
246
+ menu.append item
247
+ item
248
+ end
249
+
250
+ # This method should be implemented in subclasses to create the #menu of
251
+ # this instance. It has to be called after an instance of this class is
252
+ # created, to build the menu.
253
+ def create
254
+ raise NotImplementedError
255
+ end
256
+
257
+ def method_missing(*a, &b)
258
+ treeview.__send__(*a, &b)
259
+ end
260
+ end
261
+
262
+ # This class creates the popup menu, that opens when clicking onto the
263
+ # treeview.
264
+ class PopUpMenu
265
+ include MenuExtension
266
+
267
+ # Change the type or content of the selected node.
268
+ def change_node(item)
269
+ if current = selection.selected
270
+ parent = current.parent
271
+ old_type, old_content = current.type, current.content
272
+ if ALL_TYPES.include?(old_type)
273
+ @clipboard_data = Editor.model2data(current)
274
+ type, content = ask_for_element(parent, current.type,
275
+ current.content)
276
+ if type
277
+ current.type, current.content = type, content
278
+ current.remove_subtree(model)
279
+ toplevel.display_status("Changed a node in tree.")
280
+ window.change
281
+ end
282
+ else
283
+ toplevel.display_status(
284
+ "Cannot change node of type #{old_type} in tree!")
285
+ end
286
+ end
287
+ end
288
+
289
+ # Cut the selected node and its subtree, and save it into the
290
+ # clipboard.
291
+ def cut_node(item)
292
+ if current = selection.selected
293
+ if current and current.type == 'Key'
294
+ @clipboard_data = {
295
+ current.content => Editor.model2data(current.first_child)
296
+ }
297
+ else
298
+ @clipboard_data = Editor.model2data(current)
299
+ end
300
+ model.remove(current)
301
+ window.change
302
+ toplevel.display_status("Cut a node from tree.")
303
+ end
304
+ end
305
+
306
+ # Copy the selected node and its subtree, and save it into the
307
+ # clipboard.
308
+ def copy_node(item)
309
+ if current = selection.selected
310
+ if current and current.type == 'Key'
311
+ @clipboard_data = {
312
+ current.content => Editor.model2data(current.first_child)
313
+ }
314
+ else
315
+ @clipboard_data = Editor.model2data(current)
316
+ end
317
+ window.change
318
+ toplevel.display_status("Copied a node from tree.")
319
+ end
320
+ end
321
+
322
+ # Paste the data in the clipboard into the selected Array or Hash by
323
+ # appending it.
324
+ def paste_node_appending(item)
325
+ if current = selection.selected
326
+ if @clipboard_data
327
+ case current.type
328
+ when 'Array'
329
+ Editor.data2model(@clipboard_data, model, current)
330
+ expand_collapse(current)
331
+ when 'Hash'
332
+ if @clipboard_data.is_a? Hash
333
+ parent = current.parent
334
+ hash = Editor.model2data(current)
335
+ model.remove(current)
336
+ hash.update(@clipboard_data)
337
+ Editor.data2model(hash, model, parent)
338
+ if parent
339
+ expand_collapse(parent)
340
+ elsif @expanded
341
+ expand_all
342
+ end
343
+ window.change
344
+ else
345
+ toplevel.display_status(
346
+ "Cannot paste non-#{current.type} data into '#{current.type}'!")
347
+ end
348
+ else
349
+ toplevel.display_status(
350
+ "Cannot paste node below '#{current.type}'!")
351
+ end
352
+ else
353
+ toplevel.display_status("Nothing to paste in clipboard!")
354
+ end
355
+ else
356
+ toplevel.display_status("Append a node into the root first!")
357
+ end
358
+ end
359
+
360
+ # Paste the data in the clipboard into the selected Array inserting it
361
+ # before the selected element.
362
+ def paste_node_inserting_before(item)
363
+ if current = selection.selected
364
+ if @clipboard_data
365
+ parent = current.parent or return
366
+ parent_type = parent.type
367
+ if parent_type == 'Array'
368
+ selected_index = parent.each_with_index do |c, i|
369
+ break i if c == current
370
+ end
371
+ Editor.data2model(@clipboard_data, model, parent) do |m|
372
+ m.insert_before(parent, current)
373
+ end
374
+ expand_collapse(current)
375
+ toplevel.display_status("Inserted an element to " +
376
+ "'#{parent_type}' before index #{selected_index}.")
377
+ window.change
378
+ else
379
+ toplevel.display_status(
380
+ "Cannot insert node below '#{parent_type}'!")
381
+ end
382
+ else
383
+ toplevel.display_status("Nothing to paste in clipboard!")
384
+ end
385
+ else
386
+ toplevel.display_status("Append a node into the root first!")
387
+ end
388
+ end
389
+
390
+ # Append a new node to the selected Hash or Array.
391
+ def append_new_node(item)
392
+ if parent = selection.selected
393
+ parent_type = parent.type
394
+ case parent_type
395
+ when 'Hash'
396
+ key, type, content = ask_for_hash_pair(parent)
397
+ key or return
398
+ iter = create_node(parent, 'Key', key)
399
+ iter = create_node(iter, type, content)
400
+ toplevel.display_status(
401
+ "Added a (key, value)-pair to '#{parent_type}'.")
402
+ window.change
403
+ when 'Array'
404
+ type, content = ask_for_element(parent)
405
+ type or return
406
+ iter = create_node(parent, type, content)
407
+ window.change
408
+ toplevel.display_status("Appendend an element to '#{parent_type}'.")
409
+ else
410
+ toplevel.display_status("Cannot append to '#{parent_type}'!")
411
+ end
412
+ else
413
+ type, content = ask_for_element
414
+ type or return
415
+ iter = create_node(nil, type, content)
416
+ window.change
417
+ end
418
+ end
419
+
420
+ # Insert a new node into an Array before the selected element.
421
+ def insert_new_node(item)
422
+ if current = selection.selected
423
+ parent = current.parent or return
424
+ parent_parent = parent.parent
425
+ parent_type = parent.type
426
+ if parent_type == 'Array'
427
+ selected_index = parent.each_with_index do |c, i|
428
+ break i if c == current
429
+ end
430
+ type, content = ask_for_element(parent)
431
+ type or return
432
+ iter = model.insert_before(parent, current)
433
+ iter.type, iter.content = type, content
434
+ toplevel.display_status("Inserted an element to " +
435
+ "'#{parent_type}' before index #{selected_index}.")
436
+ window.change
437
+ else
438
+ toplevel.display_status(
439
+ "Cannot insert node below '#{parent_type}'!")
440
+ end
441
+ else
442
+ toplevel.display_status("Append a node into the root first!")
443
+ end
444
+ end
445
+
446
+ # Recursively collapse/expand a subtree starting from the selected node.
447
+ def collapse_expand(item)
448
+ if current = selection.selected
449
+ if row_expanded?(current.path)
450
+ collapse_row(current.path)
451
+ else
452
+ expand_row(current.path, true)
453
+ end
454
+ else
455
+ toplevel.display_status("Append a node into the root first!")
456
+ end
457
+ end
458
+
459
+ # Create the menu.
460
+ def create
461
+ add_item("Change node", ?n, &method(:change_node))
462
+ add_separator
463
+ add_item("Cut node", ?x, &method(:cut_node))
464
+ add_item("Copy node", ?c, &method(:copy_node))
465
+ add_item("Paste node (appending)", ?v, &method(:paste_node_appending))
466
+ add_item("Paste node (inserting before)", ?V,
467
+ &method(:paste_node_inserting_before))
468
+ add_separator
469
+ add_item("Append new node", ?a, &method(:append_new_node))
470
+ add_item("Insert new node before", ?i, &method(:insert_new_node))
471
+ add_separator
472
+ add_item("Collapse/Expand node (recursively)", ?C,
473
+ &method(:collapse_expand))
474
+
475
+ menu.show_all
476
+ signal_connect(:button_press_event) do |widget, event|
477
+ if event.kind_of? Gdk::EventButton and event.button == 3
478
+ menu.popup(nil, nil, event.button, event.time)
479
+ end
480
+ end
481
+ signal_connect(:popup_menu) do
482
+ menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
483
+ end
484
+ end
485
+ end
486
+
487
+ # This class creates the File pulldown menu.
488
+ class FileMenu
489
+ include MenuExtension
490
+
491
+ # Clear the model and filename, but ask to save the JSON document, if
492
+ # unsaved changes have occured.
493
+ def new(item)
494
+ window.clear
495
+ end
496
+
497
+ # Open a file and load it into the editor. Ask to save the JSON document
498
+ # first, if unsaved changes have occured.
499
+ def open(item)
500
+ window.file_open
501
+ end
502
+
503
+ def open_location(item)
504
+ window.location_open
505
+ end
506
+
507
+ # Revert the current JSON document in the editor to the saved version.
508
+ def revert(item)
509
+ window.instance_eval do
510
+ @filename and file_open(@filename)
511
+ end
512
+ end
513
+
514
+ # Save the current JSON document.
515
+ def save(item)
516
+ window.file_save
517
+ end
518
+
519
+ # Save the current JSON document under the given filename.
520
+ def save_as(item)
521
+ window.file_save_as
522
+ end
523
+
524
+ # Quit the editor, after asking to save any unsaved changes first.
525
+ def quit(item)
526
+ window.quit
527
+ end
528
+
529
+ # Create the menu.
530
+ def create
531
+ title = MenuItem.new('File')
532
+ title.submenu = menu
533
+ add_item('New', &method(:new))
534
+ add_item('Open', ?o, &method(:open))
535
+ add_item('Open location', ?l, &method(:open_location))
536
+ add_item('Revert', &method(:revert))
537
+ add_separator
538
+ add_item('Save', ?s, &method(:save))
539
+ add_item('Save As', ?S, &method(:save_as))
540
+ add_separator
541
+ add_item('Quit', ?q, &method(:quit))
542
+ title
543
+ end
544
+ end
545
+
546
+ # This class creates the Edit pulldown menu.
547
+ class EditMenu
548
+ include MenuExtension
549
+
550
+ # Find a string in all nodes' contents and select the found node in the
551
+ # treeview.
552
+ def find(item)
553
+ search = ask_for_find_term or return
554
+ begin
555
+ @search = Regexp.new(search)
556
+ rescue => e
557
+ Editor.error_dialog(self, "Evaluation of regex /#{search}/ failed: #{e}!")
558
+ return
559
+ end
560
+ iter = model.get_iter('0')
561
+ iter.recursive_each do |i|
562
+ if @iter
563
+ if @iter != i
564
+ next
565
+ else
566
+ @iter = nil
567
+ next
568
+ end
569
+ elsif @search.match(i[CONTENT_COL])
570
+ set_cursor(i.path, nil, false)
571
+ @iter = i
572
+ break
573
+ end
574
+ end
575
+ end
576
+
577
+ # Repeat the last search given by #find.
578
+ def find_again(item)
579
+ @search or return
580
+ iter = model.get_iter('0')
581
+ iter.recursive_each do |i|
582
+ if @iter
583
+ if @iter != i
584
+ next
585
+ else
586
+ @iter = nil
587
+ next
588
+ end
589
+ elsif @search.match(i[CONTENT_COL])
590
+ set_cursor(i.path, nil, false)
591
+ @iter = i
592
+ break
593
+ end
594
+ end
595
+ end
596
+
597
+ # Sort (Reverse sort) all elements of the selected array by the given
598
+ # expression. _x_ is the element in question.
599
+ def sort(item)
600
+ if current = selection.selected
601
+ if current.type == 'Array'
602
+ parent = current.parent
603
+ ary = Editor.model2data(current)
604
+ order, reverse = ask_for_order
605
+ order or return
606
+ begin
607
+ block = eval "lambda { |x| #{order} }"
608
+ if reverse
609
+ ary.sort! { |a,b| block[b] <=> block[a] }
610
+ else
611
+ ary.sort! { |a,b| block[a] <=> block[b] }
612
+ end
613
+ rescue => e
614
+ Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
615
+ else
616
+ Editor.data2model(ary, model, parent) do |m|
617
+ m.insert_before(parent, current)
618
+ end
619
+ model.remove(current)
620
+ expand_collapse(parent)
621
+ window.change
622
+ toplevel.display_status("Array has been sorted.")
623
+ end
624
+ else
625
+ toplevel.display_status("Only Array nodes can be sorted!")
626
+ end
627
+ else
628
+ toplevel.display_status("Select an Array to sort first!")
629
+ end
630
+ end
631
+
632
+ # Create the menu.
633
+ def create
634
+ title = MenuItem.new('Edit')
635
+ title.submenu = menu
636
+ add_item('Find', ?f, &method(:find))
637
+ add_item('Find Again', ?g, &method(:find_again))
638
+ add_separator
639
+ add_item('Sort', ?S, &method(:sort))
640
+ title
641
+ end
642
+ end
643
+
644
+ class OptionsMenu
645
+ include MenuExtension
646
+
647
+ # Collapse/Expand all nodes by default.
648
+ def collapsed_nodes(item)
649
+ if expanded
650
+ self.expanded = false
651
+ collapse_all
652
+ else
653
+ self.expanded = true
654
+ expand_all
655
+ end
656
+ end
657
+
658
+ # Toggle pretty saving mode on/off.
659
+ def pretty_saving(item)
660
+ @pretty_item.toggled
661
+ window.change
662
+ end
663
+
664
+ attr_reader :pretty_item
665
+
666
+ # Create the menu.
667
+ def create
668
+ title = MenuItem.new('Options')
669
+ title.submenu = menu
670
+ add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes))
671
+ @pretty_item = add_item('Pretty saving', nil, CheckMenuItem,
672
+ &method(:pretty_saving))
673
+ @pretty_item.active = true
674
+ window.unchange
675
+ title
676
+ end
677
+ end
678
+
679
+ # This class inherits from Gtk::TreeView, to configure it and to add a lot
680
+ # of behaviour to it.
681
+ class JSONTreeView < Gtk::TreeView
682
+ include Gtk
683
+
684
+ # Creates a JSONTreeView instance, the parameter _window_ is
685
+ # a MainWindow instance and used for self delegation.
686
+ def initialize(window)
687
+ @window = window
688
+ super(TreeStore.new(Gdk::Pixbuf, String, String))
689
+ self.selection.mode = SELECTION_BROWSE
690
+
691
+ @expanded = false
692
+ self.headers_visible = false
693
+ add_columns
694
+ add_popup_menu
695
+ end
696
+
697
+ # Returns the MainWindow instance of this JSONTreeView.
698
+ attr_reader :window
699
+
700
+ # Returns true, if nodes are autoexpanding, false otherwise.
701
+ attr_accessor :expanded
702
+
703
+ private
704
+
705
+ def add_columns
706
+ cell = CellRendererPixbuf.new
707
+ column = TreeViewColumn.new('Icon', cell,
708
+ 'pixbuf' => ICON_COL
709
+ )
710
+ append_column(column)
711
+
712
+ cell = CellRendererText.new
713
+ column = TreeViewColumn.new('Type', cell,
714
+ 'text' => TYPE_COL
715
+ )
716
+ append_column(column)
717
+
718
+ cell = CellRendererText.new
719
+ cell.editable = true
720
+ column = TreeViewColumn.new('Content', cell,
721
+ 'text' => CONTENT_COL
722
+ )
723
+ cell.signal_connect(:edited, &method(:cell_edited))
724
+ append_column(column)
725
+ end
726
+
727
+ def unify_key(iter, key)
728
+ return unless iter.type == 'Key'
729
+ parent = iter.parent
730
+ if parent.any? { |c| c != iter and c.content == key }
731
+ old_key = key
732
+ i = 0
733
+ begin
734
+ key = sprintf("%s.%d", old_key, i += 1)
735
+ end while parent.any? { |c| c != iter and c.content == key }
736
+ end
737
+ iter.content = key
738
+ end
739
+
740
+ def cell_edited(cell, path, value)
741
+ iter = model.get_iter(path)
742
+ case iter.type
743
+ when 'Key'
744
+ unify_key(iter, value)
745
+ toplevel.display_status('Key has been changed.')
746
+ when 'FalseClass'
747
+ value.downcase!
748
+ if value == 'true'
749
+ iter.type, iter.content = 'TrueClass', 'true'
750
+ end
751
+ when 'TrueClass'
752
+ value.downcase!
753
+ if value == 'false'
754
+ iter.type, iter.content = 'FalseClass', 'false'
755
+ end
756
+ when 'Numeric'
757
+ iter.content = (Integer(value) rescue Float(value) rescue 0).to_s
758
+ when 'String'
759
+ iter.content = value
760
+ when 'Hash', 'Array'
761
+ return
762
+ else
763
+ fail "Unknown type found in model: #{iter.type}"
764
+ end
765
+ window.change
766
+ end
767
+
768
+ def configure_value(value, type)
769
+ value.editable = false
770
+ case type
771
+ when 'Array', 'Hash'
772
+ value.text = ''
773
+ when 'TrueClass'
774
+ value.text = 'true'
775
+ when 'FalseClass'
776
+ value.text = 'false'
777
+ when 'NilClass'
778
+ value.text = 'null'
779
+ when 'Numeric', 'String'
780
+ value.text ||= ''
781
+ value.editable = true
782
+ else
783
+ raise ArgumentError, "unknown type '#{type}' encountered"
784
+ end
785
+ end
786
+
787
+ def add_popup_menu
788
+ menu = PopUpMenu.new(self)
789
+ menu.create
790
+ end
791
+
792
+ public
793
+
794
+ # Create a _type_ node with content _content_, and add it to _parent_
795
+ # in the model. If _parent_ is nil, create a new model and put it into
796
+ # the editor treeview.
797
+ def create_node(parent, type, content)
798
+ iter = if parent
799
+ model.append(parent)
800
+ else
801
+ new_model = Editor.data2model(nil)
802
+ toplevel.view_new_model(new_model)
803
+ new_model.iter_first
804
+ end
805
+ iter.type, iter.content = type, content
806
+ expand_collapse(parent) if parent
807
+ iter
808
+ end
809
+
810
+ # Ask for a hash key, value pair to be added to the Hash node _parent_.
811
+ def ask_for_hash_pair(parent)
812
+ key_input = type_input = value_input = nil
813
+
814
+ dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
815
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
816
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
817
+ )
818
+
819
+ hbox = HBox.new(false, 5)
820
+ hbox.pack_start(Label.new("Key:"))
821
+ hbox.pack_start(key_input = Entry.new)
822
+ key_input.text = @key || ''
823
+ dialog.vbox.add(hbox)
824
+ key_input.signal_connect(:activate) do
825
+ if parent.any? { |c| c.content == key_input.text }
826
+ toplevel.display_status('Key already exists in Hash!')
827
+ key_input.text = ''
828
+ else
829
+ toplevel.display_status('Key has been changed.')
830
+ end
831
+ end
832
+
833
+ hbox = HBox.new(false, 5)
834
+ hbox.add(Label.new("Type:"))
835
+ hbox.pack_start(type_input = ComboBox.new(true))
836
+ ALL_TYPES.each { |t| type_input.append_text(t) }
837
+ type_input.active = @type || 0
838
+ dialog.vbox.add(hbox)
839
+
840
+ type_input.signal_connect(:changed) do
841
+ value_input.editable = false
842
+ case ALL_TYPES[type_input.active]
843
+ when 'Array', 'Hash'
844
+ value_input.text = ''
845
+ when 'TrueClass'
846
+ value_input.text = 'true'
847
+ when 'FalseClass'
848
+ value_input.text = 'false'
849
+ when 'NilClass'
850
+ value_input.text = 'null'
851
+ else
852
+ value_input.text = ''
853
+ value_input.editable = true
854
+ end
855
+ end
856
+
857
+ hbox = HBox.new(false, 5)
858
+ hbox.add(Label.new("Value:"))
859
+ hbox.pack_start(value_input = Entry.new)
860
+ value_input.text = @value || ''
861
+ dialog.vbox.add(hbox)
862
+
863
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
864
+ dialog.show_all
865
+ self.focus = dialog
866
+ dialog.run do |response|
867
+ if response == Dialog::RESPONSE_ACCEPT
868
+ @key = key_input.text
869
+ type = ALL_TYPES[@type = type_input.active]
870
+ content = value_input.text
871
+ return @key, type, content
872
+ end
873
+ end
874
+ return
875
+ ensure
876
+ dialog.destroy
877
+ end
878
+
879
+ # Ask for an element to be appended _parent_.
880
+ def ask_for_element(parent = nil, default_type = nil, value_text = @content)
881
+ type_input = value_input = nil
882
+
883
+ dialog = Dialog.new(
884
+ "New element into #{parent ? parent.type : 'root'}",
885
+ nil, nil,
886
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
887
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
888
+ )
889
+ hbox = HBox.new(false, 5)
890
+ hbox.add(Label.new("Type:"))
891
+ hbox.pack_start(type_input = ComboBox.new(true))
892
+ default_active = 0
893
+ types = parent ? ALL_TYPES : CONTAINER_TYPES
894
+ types.each_with_index do |t, i|
895
+ type_input.append_text(t)
896
+ if t == default_type
897
+ default_active = i
898
+ end
899
+ end
900
+ type_input.active = default_active
901
+ dialog.vbox.add(hbox)
902
+ type_input.signal_connect(:changed) do
903
+ configure_value(value_input, types[type_input.active])
904
+ end
905
+
906
+ hbox = HBox.new(false, 5)
907
+ hbox.add(Label.new("Value:"))
908
+ hbox.pack_start(value_input = Entry.new)
909
+ value_input.text = value_text if value_text
910
+ configure_value(value_input, types[type_input.active])
911
+
912
+ dialog.vbox.add(hbox)
913
+
914
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
915
+ dialog.show_all
916
+ self.focus = dialog
917
+ dialog.run do |response|
918
+ if response == Dialog::RESPONSE_ACCEPT
919
+ type = types[type_input.active]
920
+ @content = case type
921
+ when 'Numeric'
922
+ Integer(value_input.text) rescue Float(value_input.text) rescue 0
923
+ else
924
+ value_input.text
925
+ end.to_s
926
+ return type, @content
927
+ end
928
+ end
929
+ return
930
+ ensure
931
+ dialog.destroy if dialog
932
+ end
933
+
934
+ # Ask for an order criteria for sorting, using _x_ for the element in
935
+ # question. Returns the order criterium, and true/false for reverse
936
+ # sorting.
937
+ def ask_for_order
938
+ dialog = Dialog.new(
939
+ "Give an order criterium for 'x'.",
940
+ nil, nil,
941
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
942
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
943
+ )
944
+ hbox = HBox.new(false, 5)
945
+
946
+ hbox.add(Label.new("Order:"))
947
+ hbox.pack_start(order_input = Entry.new)
948
+ order_input.text = @order || 'x'
949
+
950
+ hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'))
951
+
952
+ dialog.vbox.add(hbox)
953
+
954
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
955
+ dialog.show_all
956
+ self.focus = dialog
957
+ dialog.run do |response|
958
+ if response == Dialog::RESPONSE_ACCEPT
959
+ return @order = order_input.text, reverse_checkbox.active?
960
+ end
961
+ end
962
+ return
963
+ ensure
964
+ dialog.destroy if dialog
965
+ end
966
+
967
+ # Ask for a find term to search for in the tree. Returns the term as a
968
+ # string.
969
+ def ask_for_find_term
970
+ dialog = Dialog.new(
971
+ "Find a node matching regex in tree.",
972
+ nil, nil,
973
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
974
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
975
+ )
976
+ hbox = HBox.new(false, 5)
977
+
978
+ hbox.add(Label.new("Regex:"))
979
+ hbox.pack_start(regex_input = Entry.new)
980
+ regex_input.text = @regex || ''
981
+
982
+ dialog.vbox.add(hbox)
983
+
984
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
985
+ dialog.show_all
986
+ self.focus = dialog
987
+ dialog.run do |response|
988
+ if response == Dialog::RESPONSE_ACCEPT
989
+ return @regex = regex_input.text
990
+ end
991
+ end
992
+ return
993
+ ensure
994
+ dialog.destroy if dialog
995
+ end
996
+
997
+ # Expand or collapse row pointed to by _iter_ according
998
+ # to the #expanded attribute.
999
+ def expand_collapse(iter)
1000
+ if expanded
1001
+ expand_row(iter.path, true)
1002
+ else
1003
+ collapse_row(iter.path)
1004
+ end
1005
+ end
1006
+ end
1007
+
1008
+ # The editor main window
1009
+ class MainWindow < Gtk::Window
1010
+ include Gtk
1011
+
1012
+ def initialize(encoding)
1013
+ @changed = false
1014
+ @encoding = encoding
1015
+ super(TOPLEVEL)
1016
+ display_title
1017
+ set_default_size(800, 600)
1018
+ signal_connect(:delete_event) { quit }
1019
+
1020
+ vbox = VBox.new(false, 0)
1021
+ add(vbox)
1022
+ #vbox.border_width = 0
1023
+
1024
+ @treeview = JSONTreeView.new(self)
1025
+ @treeview.signal_connect(:'cursor-changed') do
1026
+ display_status('')
1027
+ end
1028
+
1029
+ menu_bar = create_menu_bar
1030
+ vbox.pack_start(menu_bar, false, false, 0)
1031
+
1032
+ sw = ScrolledWindow.new(nil, nil)
1033
+ sw.shadow_type = SHADOW_ETCHED_IN
1034
+ sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
1035
+ vbox.pack_start(sw, true, true, 0)
1036
+ sw.add(@treeview)
1037
+
1038
+ @status_bar = Statusbar.new
1039
+ vbox.pack_start(@status_bar, false, false, 0)
1040
+
1041
+ @filename ||= nil
1042
+ if @filename
1043
+ data = read_data(@filename)
1044
+ view_new_model Editor.data2model(data)
1045
+ end
1046
+ end
1047
+
1048
+ # Creates the menu bar with the pulldown menus and returns it.
1049
+ def create_menu_bar
1050
+ menu_bar = MenuBar.new
1051
+ @file_menu = FileMenu.new(@treeview)
1052
+ menu_bar.append @file_menu.create
1053
+ @edit_menu = EditMenu.new(@treeview)
1054
+ menu_bar.append @edit_menu.create
1055
+ @options_menu = OptionsMenu.new(@treeview)
1056
+ menu_bar.append @options_menu.create
1057
+ menu_bar
1058
+ end
1059
+
1060
+ # Sets editor status to changed, to indicate that the edited data
1061
+ # containts unsaved changes.
1062
+ def change
1063
+ @changed = true
1064
+ display_title
1065
+ end
1066
+
1067
+ # Sets editor status to unchanged, to indicate that the edited data
1068
+ # doesn't containt unsaved changes.
1069
+ def unchange
1070
+ @changed = false
1071
+ display_title
1072
+ end
1073
+
1074
+ # Puts a new model _model_ into the Gtk::TreeView to be edited.
1075
+ def view_new_model(model)
1076
+ @treeview.model = model
1077
+ @treeview.expanded = true
1078
+ @treeview.expand_all
1079
+ unchange
1080
+ end
1081
+
1082
+ # Displays _text_ in the status bar.
1083
+ def display_status(text)
1084
+ @cid ||= nil
1085
+ @status_bar.pop(@cid) if @cid
1086
+ @cid = @status_bar.get_context_id('dummy')
1087
+ @status_bar.push(@cid, text)
1088
+ end
1089
+
1090
+ # Opens a dialog, asking, if changes should be saved to a file.
1091
+ def ask_save
1092
+ if Editor.question_dialog(self,
1093
+ "Unsaved changes to JSON model. Save?")
1094
+ if @filename
1095
+ file_save
1096
+ else
1097
+ file_save_as
1098
+ end
1099
+ end
1100
+ end
1101
+
1102
+ # Quit this editor, that is, leave this editor's main loop.
1103
+ def quit
1104
+ ask_save if @changed
1105
+ destroy
1106
+ Gtk.main_quit
1107
+ true
1108
+ end
1109
+
1110
+ # Display the new title according to the editor's current state.
1111
+ def display_title
1112
+ title = TITLE.dup
1113
+ title << ": #@filename" if @filename
1114
+ title << " *" if @changed
1115
+ self.title = title
1116
+ end
1117
+
1118
+ # Clear the current model, after asking to save all unsaved changes.
1119
+ def clear
1120
+ ask_save if @changed
1121
+ @filename = nil
1122
+ self.view_new_model nil
1123
+ end
1124
+
1125
+ def check_pretty_printed(json)
1126
+ pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
1127
+ @options_menu.pretty_item.active = pretty
1128
+ end
1129
+ private :check_pretty_printed
1130
+
1131
+ # Open the data at the location _uri_, if given. Otherwise open a dialog
1132
+ # to ask for the _uri_.
1133
+ def location_open(uri = nil)
1134
+ uri = ask_for_location unless uri
1135
+ uri or return
1136
+ data = load_location(uri) or return
1137
+ view_new_model Editor.data2model(data)
1138
+ end
1139
+
1140
+ # Open the file _filename_ or call the #select_file method to ask for a
1141
+ # filename.
1142
+ def file_open(filename = nil)
1143
+ filename = select_file('Open as a JSON file') unless filename
1144
+ data = load_file(filename) or return
1145
+ view_new_model Editor.data2model(data)
1146
+ end
1147
+
1148
+ # Save the current file.
1149
+ def file_save
1150
+ if @filename
1151
+ store_file(@filename)
1152
+ else
1153
+ file_save_as
1154
+ end
1155
+ end
1156
+
1157
+ # Save the current file as the filename
1158
+ def file_save_as
1159
+ filename = select_file('Save as a JSON file')
1160
+ store_file(filename)
1161
+ end
1162
+
1163
+ # Store the current JSON document to _path_.
1164
+ def store_file(path)
1165
+ if path
1166
+ data = Editor.model2data(@treeview.model.iter_first)
1167
+ File.open(path + '.tmp', 'wb') do |output|
1168
+ if @options_menu.pretty_item.active?
1169
+ output.puts JSON.pretty_generate(data)
1170
+ else
1171
+ output.write JSON.unparse(data)
1172
+ end
1173
+ end
1174
+ File.rename path + '.tmp', path
1175
+ @filename = path
1176
+ toplevel.display_status("Saved data to '#@filename'.")
1177
+ unchange
1178
+ end
1179
+ rescue SystemCallError => e
1180
+ Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
1181
+ end
1182
+
1183
+ # Load the file named _filename_ into the editor as a JSON document.
1184
+ def load_file(filename)
1185
+ if filename
1186
+ if File.directory?(filename)
1187
+ Editor.error_dialog(self, "Try to select a JSON file!")
1188
+ return
1189
+ else
1190
+ data = read_data(filename)
1191
+ @filename = filename
1192
+ toplevel.display_status("Loaded data from '#@filename'.")
1193
+ display_title
1194
+ return data
1195
+ end
1196
+ end
1197
+ end
1198
+
1199
+ # Load the data at location _uri_ into the editor as a JSON document.
1200
+ def load_location(uri)
1201
+ data = read_data(uri)
1202
+ @filename = nil
1203
+ toplevel.display_status("Loaded data from '#{uri}'.")
1204
+ display_title
1205
+ data
1206
+ end
1207
+
1208
+ # Read a JSON document from the file named _filename_, parse it into a
1209
+ # ruby data structure, and return the data.
1210
+ def read_data(filename)
1211
+ open(filename) do |f|
1212
+ json = f.read
1213
+ check_pretty_printed(json)
1214
+ if @encoding && !/^utf8$/i.match(@encoding)
1215
+ iconverter = Iconv.new('utf8', @encoding)
1216
+ json = iconverter.iconv(json)
1217
+ end
1218
+ return JSON::parse(json, :max_nesting => false)
1219
+ end
1220
+ rescue JSON::JSONError => e
1221
+ Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
1222
+ return
1223
+ rescue SystemCallError => e
1224
+ quit
1225
+ end
1226
+
1227
+ # Open a file selecton dialog, displaying _message_, and return the
1228
+ # selected filename or nil, if no file was selected.
1229
+ def select_file(message)
1230
+ filename = nil
1231
+ fs = FileSelection.new(message).set_modal(true).
1232
+ set_filename(Dir.pwd + "/").set_transient_for(self)
1233
+ fs.signal_connect(:destroy) { Gtk.main_quit }
1234
+ fs.ok_button.signal_connect(:clicked) do
1235
+ filename = fs.filename
1236
+ fs.destroy
1237
+ Gtk.main_quit
1238
+ end
1239
+ fs.cancel_button.signal_connect(:clicked) do
1240
+ fs.destroy
1241
+ Gtk.main_quit
1242
+ end
1243
+ fs.show_all
1244
+ Gtk.main
1245
+ filename
1246
+ end
1247
+
1248
+ # Ask for location URI a to load data from. Returns the URI as a string.
1249
+ def ask_for_location
1250
+ dialog = Dialog.new(
1251
+ "Load data from location...",
1252
+ nil, nil,
1253
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
1254
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
1255
+ )
1256
+ hbox = HBox.new(false, 5)
1257
+
1258
+ hbox.add(Label.new("Location:"))
1259
+ hbox.pack_start(location_input = Entry.new)
1260
+ location_input.width_chars = 60
1261
+ location_input.text = @location || ''
1262
+
1263
+ dialog.vbox.add(hbox)
1264
+
1265
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
1266
+ dialog.show_all
1267
+ dialog.run do |response|
1268
+ if response == Dialog::RESPONSE_ACCEPT
1269
+ return @location = location_input.text
1270
+ end
1271
+ end
1272
+ return
1273
+ ensure
1274
+ dialog.destroy if dialog
1275
+ end
1276
+ end
1277
+
1278
+ class << self
1279
+ # Starts a JSON Editor. If a block was given, it yields
1280
+ # to the JSON::Editor::MainWindow instance.
1281
+ def start(encoding = nil) # :yield: window
1282
+ encoding ||= 'utf8'
1283
+ Gtk.init
1284
+ @window = Editor::MainWindow.new(encoding)
1285
+ @window.icon_list = [ Editor.fetch_icon('json') ]
1286
+ yield @window if block_given?
1287
+ @window.show_all
1288
+ Gtk.main
1289
+ end
1290
+
1291
+ attr_reader :window
1292
+ end
1293
+ end
1294
+ end
1295
+ # vim: set et sw=2 ts=2: