json 1.0.3-mswin32

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.

Files changed (84) hide show
  1. data/CHANGES +32 -0
  2. data/GPL +340 -0
  3. data/README +77 -0
  4. data/Rakefile +304 -0
  5. data/TODO +1 -0
  6. data/VERSION +1 -0
  7. data/benchmarks/benchmark.txt +133 -0
  8. data/benchmarks/benchmark_generator.rb +44 -0
  9. data/benchmarks/benchmark_parser.rb +22 -0
  10. data/benchmarks/benchmark_rails.rb +26 -0
  11. data/bin/edit_json.rb +11 -0
  12. data/bin/prettify_json.rb +75 -0
  13. data/data/example.json +1 -0
  14. data/data/index.html +37 -0
  15. data/data/prototype.js +2515 -0
  16. data/ext/json/ext/generator.so +0 -0
  17. data/ext/json/ext/generator/extconf.rb +9 -0
  18. data/ext/json/ext/generator/generator.c +729 -0
  19. data/ext/json/ext/generator/unicode.c +184 -0
  20. data/ext/json/ext/generator/unicode.h +40 -0
  21. data/ext/json/ext/parser.so +0 -0
  22. data/ext/json/ext/parser/extconf.rb +9 -0
  23. data/ext/json/ext/parser/parser.c +1554 -0
  24. data/ext/json/ext/parser/parser.rl +515 -0
  25. data/ext/json/ext/parser/unicode.c +156 -0
  26. data/ext/json/ext/parser/unicode.h +44 -0
  27. data/install.rb +26 -0
  28. data/lib/json.rb +205 -0
  29. data/lib/json/Array.xpm +21 -0
  30. data/lib/json/FalseClass.xpm +21 -0
  31. data/lib/json/Hash.xpm +21 -0
  32. data/lib/json/Key.xpm +73 -0
  33. data/lib/json/NilClass.xpm +21 -0
  34. data/lib/json/Numeric.xpm +28 -0
  35. data/lib/json/String.xpm +96 -0
  36. data/lib/json/TrueClass.xpm +21 -0
  37. data/lib/json/common.rb +184 -0
  38. data/lib/json/editor.rb +1207 -0
  39. data/lib/json/ext.rb +13 -0
  40. data/lib/json/json.xpm +1499 -0
  41. data/lib/json/pure.rb +75 -0
  42. data/lib/json/pure/generator.rb +321 -0
  43. data/lib/json/pure/parser.rb +214 -0
  44. data/lib/json/version.rb +8 -0
  45. data/tests/fixtures/fail1.json +1 -0
  46. data/tests/fixtures/fail10.json +1 -0
  47. data/tests/fixtures/fail11.json +1 -0
  48. data/tests/fixtures/fail12.json +1 -0
  49. data/tests/fixtures/fail13.json +1 -0
  50. data/tests/fixtures/fail14.json +1 -0
  51. data/tests/fixtures/fail15.json +1 -0
  52. data/tests/fixtures/fail16.json +1 -0
  53. data/tests/fixtures/fail17.json +1 -0
  54. data/tests/fixtures/fail19.json +1 -0
  55. data/tests/fixtures/fail2.json +1 -0
  56. data/tests/fixtures/fail20.json +1 -0
  57. data/tests/fixtures/fail21.json +1 -0
  58. data/tests/fixtures/fail22.json +1 -0
  59. data/tests/fixtures/fail23.json +1 -0
  60. data/tests/fixtures/fail24.json +1 -0
  61. data/tests/fixtures/fail25.json +1 -0
  62. data/tests/fixtures/fail26.json +1 -0
  63. data/tests/fixtures/fail27.json +2 -0
  64. data/tests/fixtures/fail28.json +2 -0
  65. data/tests/fixtures/fail3.json +1 -0
  66. data/tests/fixtures/fail4.json +1 -0
  67. data/tests/fixtures/fail5.json +1 -0
  68. data/tests/fixtures/fail6.json +1 -0
  69. data/tests/fixtures/fail7.json +1 -0
  70. data/tests/fixtures/fail8.json +1 -0
  71. data/tests/fixtures/fail9.json +1 -0
  72. data/tests/fixtures/pass1.json +56 -0
  73. data/tests/fixtures/pass18.json +1 -0
  74. data/tests/fixtures/pass2.json +1 -0
  75. data/tests/fixtures/pass3.json +6 -0
  76. data/tests/runner.rb +24 -0
  77. data/tests/test_json.rb +236 -0
  78. data/tests/test_json_addition.rb +94 -0
  79. data/tests/test_json_fixtures.rb +30 -0
  80. data/tests/test_json_generate.rb +81 -0
  81. data/tests/test_json_unicode.rb +58 -0
  82. data/tools/fuzz.rb +131 -0
  83. data/tools/server.rb +62 -0
  84. metadata +149 -0
@@ -0,0 +1,1207 @@
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: