json_pure 1.0.0

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