json 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of json might be problematic. Click here for more details.

@@ -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
  }
@@ -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
  #
@@ -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
@@ -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: