json 1.0.0 → 2.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES.md +503 -0
  3. data/LICENSE +56 -0
  4. data/README.md +416 -0
  5. data/ext/json/ext/fbuffer/fbuffer.h +187 -0
  6. data/ext/json/ext/generator/depend +1 -0
  7. data/ext/json/ext/generator/extconf.rb +2 -7
  8. data/ext/json/ext/generator/generator.c +1312 -338
  9. data/ext/json/ext/generator/generator.h +177 -0
  10. data/ext/json/ext/parser/depend +1 -0
  11. data/ext/json/ext/parser/extconf.rb +28 -5
  12. data/ext/json/ext/parser/parser.c +1349 -689
  13. data/ext/json/ext/parser/parser.h +96 -0
  14. data/ext/json/ext/parser/parser.rl +644 -188
  15. data/ext/json/extconf.rb +3 -0
  16. data/json.gemspec +68 -0
  17. data/lib/json/add/bigdecimal.rb +58 -0
  18. data/lib/json/add/complex.rb +51 -0
  19. data/lib/json/add/core.rb +12 -0
  20. data/lib/json/add/date.rb +54 -0
  21. data/lib/json/add/date_time.rb +67 -0
  22. data/lib/json/add/exception.rb +49 -0
  23. data/lib/json/add/ostruct.rb +54 -0
  24. data/lib/json/add/range.rb +54 -0
  25. data/lib/json/add/rational.rb +49 -0
  26. data/lib/json/add/regexp.rb +48 -0
  27. data/lib/json/add/set.rb +48 -0
  28. data/lib/json/add/struct.rb +52 -0
  29. data/lib/json/add/symbol.rb +48 -0
  30. data/lib/json/add/time.rb +59 -0
  31. data/lib/json/common.rb +588 -74
  32. data/lib/json/ext.rb +3 -1
  33. data/lib/json/generic_object.rb +75 -0
  34. data/lib/json/pure/generator.rb +311 -119
  35. data/lib/json/pure/parser.rb +182 -55
  36. data/lib/json/pure.rb +5 -65
  37. data/lib/json/version.rb +2 -1
  38. data/lib/json.rb +583 -196
  39. metadata +78 -137
  40. data/CHANGES +0 -25
  41. data/GPL +0 -340
  42. data/README +0 -77
  43. data/Rakefile +0 -250
  44. data/TODO +0 -1
  45. data/VERSION +0 -1
  46. data/benchmarks/benchmark.txt +0 -133
  47. data/benchmarks/benchmark_generator.rb +0 -44
  48. data/benchmarks/benchmark_parser.rb +0 -22
  49. data/benchmarks/benchmark_rails.rb +0 -26
  50. data/bin/edit_json.rb +0 -11
  51. data/data/example.json +0 -1
  52. data/data/index.html +0 -37
  53. data/data/prototype.js +0 -2515
  54. data/ext/json/ext/generator/Makefile +0 -149
  55. data/ext/json/ext/generator/unicode.c +0 -184
  56. data/ext/json/ext/generator/unicode.h +0 -40
  57. data/ext/json/ext/parser/Makefile +0 -149
  58. data/ext/json/ext/parser/unicode.c +0 -156
  59. data/ext/json/ext/parser/unicode.h +0 -44
  60. data/install.rb +0 -26
  61. data/lib/json/Array.xpm +0 -21
  62. data/lib/json/FalseClass.xpm +0 -21
  63. data/lib/json/Hash.xpm +0 -21
  64. data/lib/json/Key.xpm +0 -73
  65. data/lib/json/NilClass.xpm +0 -21
  66. data/lib/json/Numeric.xpm +0 -28
  67. data/lib/json/String.xpm +0 -96
  68. data/lib/json/TrueClass.xpm +0 -21
  69. data/lib/json/editor.rb +0 -1207
  70. data/lib/json/json.xpm +0 -1499
  71. data/tests/fixtures/fail1.json +0 -1
  72. data/tests/fixtures/fail10.json +0 -1
  73. data/tests/fixtures/fail11.json +0 -1
  74. data/tests/fixtures/fail12.json +0 -1
  75. data/tests/fixtures/fail13.json +0 -1
  76. data/tests/fixtures/fail14.json +0 -1
  77. data/tests/fixtures/fail15.json +0 -1
  78. data/tests/fixtures/fail16.json +0 -1
  79. data/tests/fixtures/fail17.json +0 -1
  80. data/tests/fixtures/fail19.json +0 -1
  81. data/tests/fixtures/fail2.json +0 -1
  82. data/tests/fixtures/fail20.json +0 -1
  83. data/tests/fixtures/fail21.json +0 -1
  84. data/tests/fixtures/fail22.json +0 -1
  85. data/tests/fixtures/fail23.json +0 -1
  86. data/tests/fixtures/fail24.json +0 -1
  87. data/tests/fixtures/fail25.json +0 -1
  88. data/tests/fixtures/fail26.json +0 -1
  89. data/tests/fixtures/fail27.json +0 -2
  90. data/tests/fixtures/fail28.json +0 -2
  91. data/tests/fixtures/fail3.json +0 -1
  92. data/tests/fixtures/fail4.json +0 -1
  93. data/tests/fixtures/fail5.json +0 -1
  94. data/tests/fixtures/fail6.json +0 -1
  95. data/tests/fixtures/fail7.json +0 -1
  96. data/tests/fixtures/fail8.json +0 -1
  97. data/tests/fixtures/fail9.json +0 -1
  98. data/tests/fixtures/pass1.json +0 -56
  99. data/tests/fixtures/pass18.json +0 -1
  100. data/tests/fixtures/pass2.json +0 -1
  101. data/tests/fixtures/pass3.json +0 -6
  102. data/tests/runner.rb +0 -24
  103. data/tests/test_json.rb +0 -235
  104. data/tests/test_json_addition.rb +0 -94
  105. data/tests/test_json_fixtures.rb +0 -30
  106. data/tests/test_json_generate.rb +0 -81
  107. data/tests/test_json_unicode.rb +0 -55
  108. data/tools/fuzz.rb +0 -133
  109. data/tools/server.rb +0 -62
data/lib/json/editor.rb DELETED
@@ -1,1207 +0,0 @@
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: