json 1.5.5 → 1.6.0

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

Potentially problematic release.


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

Files changed (44) hide show
  1. data/CHANGES +3 -7
  2. data/Gemfile +0 -4
  3. data/Rakefile +0 -7
  4. data/VERSION +1 -1
  5. data/ext/json/ext/parser/parser.c +18 -18
  6. data/ext/json/ext/parser/parser.rl +1 -4
  7. data/install.rb +1 -8
  8. data/java/src/json/ext/Parser.java +85 -81
  9. data/java/src/json/ext/Parser.rl +1 -1
  10. data/json.gemspec +5 -6
  11. data/json_pure.gemspec +4 -8
  12. data/lib/json/add/core.rb +9 -244
  13. data/lib/json/add/date.rb +34 -0
  14. data/lib/json/add/date_time.rb +50 -0
  15. data/lib/json/add/exception.rb +31 -0
  16. data/lib/json/add/range.rb +29 -0
  17. data/lib/json/add/regexp.rb +30 -0
  18. data/lib/json/add/struct.rb +30 -0
  19. data/lib/json/add/symbol.rb +25 -0
  20. data/lib/json/add/time.rb +35 -0
  21. data/lib/json/common.rb +5 -12
  22. data/lib/json/pure/parser.rb +4 -4
  23. data/lib/json/version.rb +1 -1
  24. data/tests/test_json.rb +2 -22
  25. data/tests/test_json_addition.rb +21 -29
  26. data/tests/test_json_string_matching.rb +6 -5
  27. data/tools/server.rb +1 -0
  28. metadata +124 -152
  29. data/0001-Security-fix-create_additons-JSON-GenericObject.patch +0 -448
  30. data/0001-Security-fix-create_additons-problem-1.5.5.patch +0 -630
  31. data/0001-Security-fix-for-create_additions-problem-1.6.8.patch +0 -685
  32. data/Gemfile.lock +0 -60
  33. data/bin/edit_json.rb +0 -9
  34. data/bin/prettify_json.rb +0 -48
  35. data/lib/json/Array.xpm +0 -21
  36. data/lib/json/FalseClass.xpm +0 -21
  37. data/lib/json/Hash.xpm +0 -21
  38. data/lib/json/Key.xpm +0 -73
  39. data/lib/json/NilClass.xpm +0 -21
  40. data/lib/json/Numeric.xpm +0 -28
  41. data/lib/json/String.xpm +0 -96
  42. data/lib/json/TrueClass.xpm +0 -21
  43. data/lib/json/editor.rb +0 -1369
  44. data/lib/json/json.xpm +0 -1499
@@ -1,60 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- json (1.5.5)
5
- json (1.5.5-java)
6
- json_pure (1.5.5)
7
- spruz (~> 0.2.8)
8
-
9
- GEM
10
- remote: http://rubygems.org/
11
- specs:
12
- bullshit (0.1.3)
13
- dslkit (~> 0.2)
14
- more_math (~> 0.0.2)
15
- tins (~> 0.3)
16
- columnize (0.3.6)
17
- debugger (1.3.0)
18
- columnize (>= 0.3.1)
19
- debugger-linecache (~> 1.1.1)
20
- debugger-ruby_core_source (~> 1.1.7)
21
- debugger-linecache (1.1.2)
22
- debugger-ruby_core_source (>= 1.1.1)
23
- debugger-ruby_core_source (1.1.7)
24
- dslkit (0.2.12)
25
- term-ansicolor (~> 1.0)
26
- tins (~> 0.6)
27
- more_math (0.0.4)
28
- tins (~> 0.3)
29
- permutation (0.1.8)
30
- pry-editline (1.1.1)
31
- rake (0.9.6)
32
- rdoc (3.12.1)
33
- json (~> 1.4)
34
- sdoc (0.3.20)
35
- json (>= 1.1.3)
36
- rdoc (~> 3.10)
37
- spruz (0.2.13)
38
- term-ansicolor (1.0.7)
39
- test-unit (2.5.4)
40
- tins (0.7.0)
41
- utils (0.0.69)
42
- dslkit (~> 0.2.10)
43
- pry-editline
44
- term-ansicolor (~> 1.0)
45
- tins (~> 0.6)
46
-
47
- PLATFORMS
48
- java
49
- ruby
50
-
51
- DEPENDENCIES
52
- bullshit
53
- debugger
54
- json!
55
- json_pure!
56
- permutation
57
- rake (~> 0.9.2)
58
- sdoc
59
- test-unit
60
- utils
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env ruby
2
- require 'json/editor'
3
-
4
- filename, encoding = ARGV
5
- JSON::Editor.start(encoding) do |window|
6
- if filename
7
- window.file_open(filename)
8
- end
9
- end
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'json'
4
- require 'fileutils'
5
- include FileUtils
6
- require 'spruz/go'
7
- include Spruz::GO
8
-
9
- opts = go 'slhi:', args = ARGV.dup
10
- if opts['h'] || opts['l'] && opts['s']
11
- puts <<EOT
12
- Usage: #{File.basename($0)} [OPTION] [FILE]
13
-
14
- If FILE is skipped, this scripts waits for input from STDIN. Otherwise
15
- FILE is opened, read, and used as input for the prettifier.
16
-
17
- OPTION can be
18
- -s to output the shortest possible JSON (precludes -l)
19
- -l to output a longer, better formatted JSON (precludes -s)
20
- -i EXT prettifies FILE in place, saving a backup to FILE.EXT
21
- -h this help
22
- EOT
23
- exit 0
24
- end
25
-
26
- json_opts = { :max_nesting => false, :create_additions => false }
27
-
28
- document =
29
- if filename = args.first or filename == '-'
30
- File.read(filename)
31
- else
32
- STDIN.read
33
- end
34
-
35
- json = JSON.parse document, json_opts
36
-
37
- output = if opts['s']
38
- JSON.fast_generate json, json_opts
39
- else # default is -l
40
- JSON.pretty_generate json, json_opts
41
- end
42
-
43
- if opts['i'] && filename
44
- cp filename, "#{filename}.#{opts['i']}"
45
- File.open(filename, 'w') { |f| f.puts output }
46
- else
47
- puts output
48
- end
@@ -1,21 +0,0 @@
1
- /* XPM */
2
- static char * Array_xpm[] = {
3
- "16 16 2 1",
4
- " c None",
5
- ". c #000000",
6
- " ",
7
- " ",
8
- " ",
9
- " .......... ",
10
- " . . ",
11
- " . . ",
12
- " . . ",
13
- " . . ",
14
- " . . ",
15
- " . . ",
16
- " . . ",
17
- " . . ",
18
- " .......... ",
19
- " ",
20
- " ",
21
- " "};
@@ -1,21 +0,0 @@
1
- /* XPM */
2
- static char * False_xpm[] = {
3
- "16 16 2 1",
4
- " c None",
5
- ". c #FF0000",
6
- " ",
7
- " ",
8
- " ",
9
- " ...... ",
10
- " . ",
11
- " . ",
12
- " . ",
13
- " ...... ",
14
- " . ",
15
- " . ",
16
- " . ",
17
- " . ",
18
- " . ",
19
- " ",
20
- " ",
21
- " "};
@@ -1,21 +0,0 @@
1
- /* XPM */
2
- static char * Hash_xpm[] = {
3
- "16 16 2 1",
4
- " c None",
5
- ". c #000000",
6
- " ",
7
- " ",
8
- " ",
9
- " . . ",
10
- " . . ",
11
- " . . ",
12
- " ......... ",
13
- " . . ",
14
- " . . ",
15
- " ......... ",
16
- " . . ",
17
- " . . ",
18
- " . . ",
19
- " ",
20
- " ",
21
- " "};
@@ -1,73 +0,0 @@
1
- /* XPM */
2
- static char * Key_xpm[] = {
3
- "16 16 54 1",
4
- " c None",
5
- ". c #110007",
6
- "+ c #0E0900",
7
- "@ c #000013",
8
- "# c #070600",
9
- "$ c #F6F006",
10
- "% c #ECE711",
11
- "& c #E5EE00",
12
- "* c #16021E",
13
- "= c #120900",
14
- "- c #EDF12B",
15
- "; c #000033",
16
- "> c #0F0000",
17
- ", c #FFFE03",
18
- "' c #E6E500",
19
- ") c #16021B",
20
- "! c #F7F502",
21
- "~ c #000E00",
22
- "{ c #130000",
23
- "] c #FFF000",
24
- "^ c #FFE711",
25
- "/ c #140005",
26
- "( c #190025",
27
- "_ c #E9DD27",
28
- ": c #E7DC04",
29
- "< c #FFEC09",
30
- "[ c #FFE707",
31
- "} c #FFDE10",
32
- "| c #150021",
33
- "1 c #160700",
34
- "2 c #FAF60E",
35
- "3 c #EFE301",
36
- "4 c #FEF300",
37
- "5 c #E7E000",
38
- "6 c #FFFF08",
39
- "7 c #0E0206",
40
- "8 c #040000",
41
- "9 c #03052E",
42
- "0 c #041212",
43
- "a c #070300",
44
- "b c #F2E713",
45
- "c c #F9DE13",
46
- "d c #36091E",
47
- "e c #00001C",
48
- "f c #1F0010",
49
- "g c #FFF500",
50
- "h c #DEDE00",
51
- "i c #050A00",
52
- "j c #FAF14A",
53
- "k c #F5F200",
54
- "l c #040404",
55
- "m c #1A0D00",
56
- "n c #EDE43D",
57
- "o c #ECE007",
58
- " ",
59
- " ",
60
- " .+@ ",
61
- " #$%&* ",
62
- " =-;>,') ",
63
- " >!~{]^/ ",
64
- " (_:<[}| ",
65
- " 1234567 ",
66
- " 890abcd ",
67
- " efghi ",
68
- " >jkl ",
69
- " mnol ",
70
- " >kl ",
71
- " ll ",
72
- " ",
73
- " "};
@@ -1,21 +0,0 @@
1
- /* XPM */
2
- static char * False_xpm[] = {
3
- "16 16 2 1",
4
- " c None",
5
- ". c #000000",
6
- " ",
7
- " ",
8
- " ",
9
- " ... ",
10
- " . . ",
11
- " . . ",
12
- " . . ",
13
- " . . ",
14
- " . . ",
15
- " . . ",
16
- " . . ",
17
- " . . ",
18
- " ... ",
19
- " ",
20
- " ",
21
- " "};
@@ -1,28 +0,0 @@
1
- /* XPM */
2
- static char * Numeric_xpm[] = {
3
- "16 16 9 1",
4
- " c None",
5
- ". c #FF0000",
6
- "+ c #0000FF",
7
- "@ c #0023DB",
8
- "# c #00EA14",
9
- "$ c #00FF00",
10
- "% c #004FAF",
11
- "& c #0028D6",
12
- "* c #00F20C",
13
- " ",
14
- " ",
15
- " ",
16
- " ... +++@#$$$$ ",
17
- " .+ %& $$ ",
18
- " . + $ ",
19
- " . + $$ ",
20
- " . ++$$$$ ",
21
- " . + $$ ",
22
- " . + $ ",
23
- " . + $ ",
24
- " . + $ $$ ",
25
- " .....++++*$$ ",
26
- " ",
27
- " ",
28
- " "};
@@ -1,96 +0,0 @@
1
- /* XPM */
2
- static char * String_xpm[] = {
3
- "16 16 77 1",
4
- " c None",
5
- ". c #000000",
6
- "+ c #040404",
7
- "@ c #080806",
8
- "# c #090606",
9
- "$ c #EEEAE1",
10
- "% c #E7E3DA",
11
- "& c #E0DBD1",
12
- "* c #D4B46F",
13
- "= c #0C0906",
14
- "- c #E3C072",
15
- "; c #E4C072",
16
- "> c #060505",
17
- ", c #0B0A08",
18
- "' c #D5B264",
19
- ") c #D3AF5A",
20
- "! c #080602",
21
- "~ c #E1B863",
22
- "{ c #DDB151",
23
- "] c #DBAE4A",
24
- "^ c #DDB152",
25
- "/ c #DDB252",
26
- "( c #070705",
27
- "_ c #0C0A07",
28
- ": c #D3A33B",
29
- "< c #020201",
30
- "[ c #DAAA41",
31
- "} c #040302",
32
- "| c #E4D9BF",
33
- "1 c #0B0907",
34
- "2 c #030201",
35
- "3 c #020200",
36
- "4 c #C99115",
37
- "5 c #080704",
38
- "6 c #DBC8A2",
39
- "7 c #E7D7B4",
40
- "8 c #E0CD9E",
41
- "9 c #080601",
42
- "0 c #040400",
43
- "a c #010100",
44
- "b c #0B0B08",
45
- "c c #DCBF83",
46
- "d c #DCBC75",
47
- "e c #DEB559",
48
- "f c #040301",
49
- "g c #BC8815",
50
- "h c #120E07",
51
- "i c #060402",
52
- "j c #0A0804",
53
- "k c #D4A747",
54
- "l c #D6A12F",
55
- "m c #0E0C05",
56
- "n c #C8C1B0",
57
- "o c #1D1B15",
58
- "p c #D7AD51",
59
- "q c #070502",
60
- "r c #080804",
61
- "s c #BC953B",
62
- "t c #C4BDAD",
63
- "u c #0B0807",
64
- "v c #DBAC47",
65
- "w c #1B150A",
66
- "x c #B78A2C",
67
- "y c #D8A83C",
68
- "z c #D4A338",
69
- "A c #0F0B03",
70
- "B c #181105",
71
- "C c #C59325",
72
- "D c #C18E1F",
73
- "E c #060600",
74
- "F c #CC992D",
75
- "G c #B98B25",
76
- "H c #B3831F",
77
- "I c #C08C1C",
78
- "J c #060500",
79
- "K c #0E0C03",
80
- "L c #0D0A00",
81
- " ",
82
- " .+@# ",
83
- " .$%&*= ",
84
- " .-;>,')! ",
85
- " .~. .{]. ",
86
- " .^/. (_:< ",
87
- " .[.}|$12 ",
88
- " 345678}90 ",
89
- " a2bcdefgh ",
90
- " ijkl.mno ",
91
- " <pq. rstu ",
92
- " .]v. wx= ",
93
- " .yzABCDE ",
94
- " .FGHIJ ",
95
- " 0KL0 ",
96
- " "};
@@ -1,21 +0,0 @@
1
- /* XPM */
2
- static char * TrueClass_xpm[] = {
3
- "16 16 2 1",
4
- " c None",
5
- ". c #0BF311",
6
- " ",
7
- " ",
8
- " ",
9
- " ......... ",
10
- " . ",
11
- " . ",
12
- " . ",
13
- " . ",
14
- " . ",
15
- " . ",
16
- " . ",
17
- " . ",
18
- " . ",
19
- " ",
20
- " ",
21
- " "};
@@ -1,1369 +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 'json'
6
- require 'rbconfig'
7
- require 'open-uri'
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
- DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event|
29
- case event.keyval
30
- when Gdk::Keyval::GDK_Return
31
- dialog.response Dialog::RESPONSE_ACCEPT
32
- when Gdk::Keyval::GDK_Escape
33
- dialog.response Dialog::RESPONSE_REJECT
34
- end
35
- end
36
-
37
- # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
38
- def Editor.fetch_icon(name)
39
- @icon_cache ||= {}
40
- unless @icon_cache.key?(name)
41
- path = File.dirname(__FILE__)
42
- @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
43
- end
44
- @icon_cache[name]
45
- end
46
-
47
- # Opens an error dialog on top of _window_ showing the error message
48
- # _text_.
49
- def Editor.error_dialog(window, text)
50
- dialog = MessageDialog.new(window, Dialog::MODAL,
51
- MessageDialog::ERROR,
52
- MessageDialog::BUTTONS_CLOSE, text)
53
- dialog.show_all
54
- dialog.run
55
- rescue TypeError
56
- dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
57
- MessageDialog::ERROR,
58
- MessageDialog::BUTTONS_CLOSE, text)
59
- dialog.show_all
60
- dialog.run
61
- ensure
62
- dialog.destroy if dialog
63
- end
64
-
65
- # Opens a yes/no question dialog on top of _window_ showing the error
66
- # message _text_. If yes was answered _true_ is returned, otherwise
67
- # _false_.
68
- def Editor.question_dialog(window, text)
69
- dialog = MessageDialog.new(window, Dialog::MODAL,
70
- MessageDialog::QUESTION,
71
- MessageDialog::BUTTONS_YES_NO, text)
72
- dialog.show_all
73
- dialog.run do |response|
74
- return Gtk::Dialog::RESPONSE_YES === response
75
- end
76
- ensure
77
- dialog.destroy if dialog
78
- end
79
-
80
- # Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
81
- # data structure and return it.
82
- def Editor.model2data(iter)
83
- return nil if iter.nil?
84
- case iter.type
85
- when 'Hash'
86
- hash = {}
87
- iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
88
- hash
89
- when 'Array'
90
- array = Array.new(iter.n_children)
91
- iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
92
- array
93
- when 'Key'
94
- iter.content
95
- when 'String'
96
- iter.content
97
- when 'Numeric'
98
- content = iter.content
99
- if /\./.match(content)
100
- content.to_f
101
- else
102
- content.to_i
103
- end
104
- when 'TrueClass'
105
- true
106
- when 'FalseClass'
107
- false
108
- when 'NilClass'
109
- nil
110
- else
111
- fail "Unknown type found in model: #{iter.type}"
112
- end
113
- end
114
-
115
- # Convert the Ruby data structure _data_ into tree model data for Gtk and
116
- # returns the whole model. If the parameter _model_ wasn't given a new
117
- # Gtk::TreeStore is created as the model. The _parent_ parameter specifies
118
- # the parent node (iter, Gtk:TreeIter instance) to which the data is
119
- # appended, alternativeley the result of the yielded block is used as iter.
120
- def Editor.data2model(data, model = nil, parent = nil)
121
- model ||= TreeStore.new(Gdk::Pixbuf, String, String)
122
- iter = if block_given?
123
- yield model
124
- else
125
- model.append(parent)
126
- end
127
- case data
128
- when Hash
129
- iter.type = 'Hash'
130
- data.sort.each do |key, value|
131
- pair_iter = model.append(iter)
132
- pair_iter.type = 'Key'
133
- pair_iter.content = key.to_s
134
- Editor.data2model(value, model, pair_iter)
135
- end
136
- when Array
137
- iter.type = 'Array'
138
- data.each do |value|
139
- Editor.data2model(value, model, iter)
140
- end
141
- when Numeric
142
- iter.type = 'Numeric'
143
- iter.content = data.to_s
144
- when String, true, false, nil
145
- iter.type = data.class.name
146
- iter.content = data.nil? ? 'null' : data.to_s
147
- else
148
- iter.type = 'String'
149
- iter.content = data.to_s
150
- end
151
- model
152
- end
153
-
154
- # The Gtk::TreeIter class is reopened and some auxiliary methods are added.
155
- class Gtk::TreeIter
156
- include Enumerable
157
-
158
- # Traverse each of this Gtk::TreeIter instance's children
159
- # and yield to them.
160
- def each
161
- n_children.times { |i| yield nth_child(i) }
162
- end
163
-
164
- # Recursively traverse all nodes of this Gtk::TreeIter's subtree
165
- # (including self) and yield to them.
166
- def recursive_each(&block)
167
- yield self
168
- each do |i|
169
- i.recursive_each(&block)
170
- end
171
- end
172
-
173
- # Remove the subtree of this Gtk::TreeIter instance from the
174
- # model _model_.
175
- def remove_subtree(model)
176
- while current = first_child
177
- model.remove(current)
178
- end
179
- end
180
-
181
- # Returns the type of this node.
182
- def type
183
- self[TYPE_COL]
184
- end
185
-
186
- # Sets the type of this node to _value_. This implies setting
187
- # the respective icon accordingly.
188
- def type=(value)
189
- self[TYPE_COL] = value
190
- self[ICON_COL] = Editor.fetch_icon(value)
191
- end
192
-
193
- # Returns the content of this node.
194
- def content
195
- self[CONTENT_COL]
196
- end
197
-
198
- # Sets the content of this node to _value_.
199
- def content=(value)
200
- self[CONTENT_COL] = value
201
- end
202
- end
203
-
204
- # This module bundles some method, that can be used to create a menu. It
205
- # should be included into the class in question.
206
- module MenuExtension
207
- include Gtk
208
-
209
- # Creates a Menu, that includes MenuExtension. _treeview_ is the
210
- # Gtk::TreeView, on which it operates.
211
- def initialize(treeview)
212
- @treeview = treeview
213
- @menu = Menu.new
214
- end
215
-
216
- # Returns the Gtk::TreeView of this menu.
217
- attr_reader :treeview
218
-
219
- # Returns the menu.
220
- attr_reader :menu
221
-
222
- # Adds a Gtk::SeparatorMenuItem to this instance's #menu.
223
- def add_separator
224
- menu.append SeparatorMenuItem.new
225
- end
226
-
227
- # Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
228
- # string, _klass_ is the item type, and _callback_ is the procedure, that
229
- # is called if the _item_ is activated.
230
- def add_item(label, keyval = nil, klass = MenuItem, &callback)
231
- label = "#{label} (C-#{keyval.chr})" if keyval
232
- item = klass.new(label)
233
- item.signal_connect(:activate, &callback)
234
- if keyval
235
- self.signal_connect(:'key-press-event') do |item, event|
236
- if event.state & Gdk::Window::ModifierType::CONTROL_MASK != 0 and
237
- event.keyval == keyval
238
- callback.call item
239
- end
240
- end
241
- end
242
- menu.append item
243
- item
244
- end
245
-
246
- # This method should be implemented in subclasses to create the #menu of
247
- # this instance. It has to be called after an instance of this class is
248
- # created, to build the menu.
249
- def create
250
- raise NotImplementedError
251
- end
252
-
253
- def method_missing(*a, &b)
254
- treeview.__send__(*a, &b)
255
- end
256
- end
257
-
258
- # This class creates the popup menu, that opens when clicking onto the
259
- # treeview.
260
- class PopUpMenu
261
- include MenuExtension
262
-
263
- # Change the type or content of the selected node.
264
- def change_node(item)
265
- if current = selection.selected
266
- parent = current.parent
267
- old_type, old_content = current.type, current.content
268
- if ALL_TYPES.include?(old_type)
269
- @clipboard_data = Editor.model2data(current)
270
- type, content = ask_for_element(parent, current.type,
271
- current.content)
272
- if type
273
- current.type, current.content = type, content
274
- current.remove_subtree(model)
275
- toplevel.display_status("Changed a node in tree.")
276
- window.change
277
- end
278
- else
279
- toplevel.display_status(
280
- "Cannot change node of type #{old_type} in tree!")
281
- end
282
- end
283
- end
284
-
285
- # Cut the selected node and its subtree, and save it into the
286
- # clipboard.
287
- def cut_node(item)
288
- if current = selection.selected
289
- if current and current.type == 'Key'
290
- @clipboard_data = {
291
- current.content => Editor.model2data(current.first_child)
292
- }
293
- else
294
- @clipboard_data = Editor.model2data(current)
295
- end
296
- model.remove(current)
297
- window.change
298
- toplevel.display_status("Cut a node from tree.")
299
- end
300
- end
301
-
302
- # Copy the selected node and its subtree, and save it into the
303
- # clipboard.
304
- def copy_node(item)
305
- if current = selection.selected
306
- if current and current.type == 'Key'
307
- @clipboard_data = {
308
- current.content => Editor.model2data(current.first_child)
309
- }
310
- else
311
- @clipboard_data = Editor.model2data(current)
312
- end
313
- window.change
314
- toplevel.display_status("Copied a node from tree.")
315
- end
316
- end
317
-
318
- # Paste the data in the clipboard into the selected Array or Hash by
319
- # appending it.
320
- def paste_node_appending(item)
321
- if current = selection.selected
322
- if @clipboard_data
323
- case current.type
324
- when 'Array'
325
- Editor.data2model(@clipboard_data, model, current)
326
- expand_collapse(current)
327
- when 'Hash'
328
- if @clipboard_data.is_a? Hash
329
- parent = current.parent
330
- hash = Editor.model2data(current)
331
- model.remove(current)
332
- hash.update(@clipboard_data)
333
- Editor.data2model(hash, model, parent)
334
- if parent
335
- expand_collapse(parent)
336
- elsif @expanded
337
- expand_all
338
- end
339
- window.change
340
- else
341
- toplevel.display_status(
342
- "Cannot paste non-#{current.type} data into '#{current.type}'!")
343
- end
344
- else
345
- toplevel.display_status(
346
- "Cannot paste node below '#{current.type}'!")
347
- end
348
- else
349
- toplevel.display_status("Nothing to paste in clipboard!")
350
- end
351
- else
352
- toplevel.display_status("Append a node into the root first!")
353
- end
354
- end
355
-
356
- # Paste the data in the clipboard into the selected Array inserting it
357
- # before the selected element.
358
- def paste_node_inserting_before(item)
359
- if current = selection.selected
360
- if @clipboard_data
361
- parent = current.parent or return
362
- parent_type = parent.type
363
- if parent_type == 'Array'
364
- selected_index = parent.each_with_index do |c, i|
365
- break i if c == current
366
- end
367
- Editor.data2model(@clipboard_data, model, parent) do |m|
368
- m.insert_before(parent, current)
369
- end
370
- expand_collapse(current)
371
- toplevel.display_status("Inserted an element to " +
372
- "'#{parent_type}' before index #{selected_index}.")
373
- window.change
374
- else
375
- toplevel.display_status(
376
- "Cannot insert node below '#{parent_type}'!")
377
- end
378
- else
379
- toplevel.display_status("Nothing to paste in clipboard!")
380
- end
381
- else
382
- toplevel.display_status("Append a node into the root first!")
383
- end
384
- end
385
-
386
- # Append a new node to the selected Hash or Array.
387
- def append_new_node(item)
388
- if parent = selection.selected
389
- parent_type = parent.type
390
- case parent_type
391
- when 'Hash'
392
- key, type, content = ask_for_hash_pair(parent)
393
- key or return
394
- iter = create_node(parent, 'Key', key)
395
- iter = create_node(iter, type, content)
396
- toplevel.display_status(
397
- "Added a (key, value)-pair to '#{parent_type}'.")
398
- window.change
399
- when 'Array'
400
- type, content = ask_for_element(parent)
401
- type or return
402
- iter = create_node(parent, type, content)
403
- window.change
404
- toplevel.display_status("Appendend an element to '#{parent_type}'.")
405
- else
406
- toplevel.display_status("Cannot append to '#{parent_type}'!")
407
- end
408
- else
409
- type, content = ask_for_element
410
- type or return
411
- iter = create_node(nil, type, content)
412
- window.change
413
- end
414
- end
415
-
416
- # Insert a new node into an Array before the selected element.
417
- def insert_new_node(item)
418
- if current = selection.selected
419
- parent = current.parent or return
420
- parent_parent = parent.parent
421
- parent_type = parent.type
422
- if parent_type == 'Array'
423
- selected_index = parent.each_with_index do |c, i|
424
- break i if c == current
425
- end
426
- type, content = ask_for_element(parent)
427
- type or return
428
- iter = model.insert_before(parent, current)
429
- iter.type, iter.content = type, content
430
- toplevel.display_status("Inserted an element to " +
431
- "'#{parent_type}' before index #{selected_index}.")
432
- window.change
433
- else
434
- toplevel.display_status(
435
- "Cannot insert node below '#{parent_type}'!")
436
- end
437
- else
438
- toplevel.display_status("Append a node into the root first!")
439
- end
440
- end
441
-
442
- # Recursively collapse/expand a subtree starting from the selected node.
443
- def collapse_expand(item)
444
- if current = selection.selected
445
- if row_expanded?(current.path)
446
- collapse_row(current.path)
447
- else
448
- expand_row(current.path, true)
449
- end
450
- else
451
- toplevel.display_status("Append a node into the root first!")
452
- end
453
- end
454
-
455
- # Create the menu.
456
- def create
457
- add_item("Change node", ?n, &method(:change_node))
458
- add_separator
459
- add_item("Cut node", ?X, &method(:cut_node))
460
- add_item("Copy node", ?C, &method(:copy_node))
461
- add_item("Paste node (appending)", ?A, &method(:paste_node_appending))
462
- add_item("Paste node (inserting before)", ?I,
463
- &method(:paste_node_inserting_before))
464
- add_separator
465
- add_item("Append new node", ?a, &method(:append_new_node))
466
- add_item("Insert new node before", ?i, &method(:insert_new_node))
467
- add_separator
468
- add_item("Collapse/Expand node (recursively)", ?e,
469
- &method(:collapse_expand))
470
-
471
- menu.show_all
472
- signal_connect(:button_press_event) do |widget, event|
473
- if event.kind_of? Gdk::EventButton and event.button == 3
474
- menu.popup(nil, nil, event.button, event.time)
475
- end
476
- end
477
- signal_connect(:popup_menu) do
478
- menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
479
- end
480
- end
481
- end
482
-
483
- # This class creates the File pulldown menu.
484
- class FileMenu
485
- include MenuExtension
486
-
487
- # Clear the model and filename, but ask to save the JSON document, if
488
- # unsaved changes have occured.
489
- def new(item)
490
- window.clear
491
- end
492
-
493
- # Open a file and load it into the editor. Ask to save the JSON document
494
- # first, if unsaved changes have occured.
495
- def open(item)
496
- window.file_open
497
- end
498
-
499
- def open_location(item)
500
- window.location_open
501
- end
502
-
503
- # Revert the current JSON document in the editor to the saved version.
504
- def revert(item)
505
- window.instance_eval do
506
- @filename and file_open(@filename)
507
- end
508
- end
509
-
510
- # Save the current JSON document.
511
- def save(item)
512
- window.file_save
513
- end
514
-
515
- # Save the current JSON document under the given filename.
516
- def save_as(item)
517
- window.file_save_as
518
- end
519
-
520
- # Quit the editor, after asking to save any unsaved changes first.
521
- def quit(item)
522
- window.quit
523
- end
524
-
525
- # Create the menu.
526
- def create
527
- title = MenuItem.new('File')
528
- title.submenu = menu
529
- add_item('New', &method(:new))
530
- add_item('Open', ?o, &method(:open))
531
- add_item('Open location', ?l, &method(:open_location))
532
- add_item('Revert', &method(:revert))
533
- add_separator
534
- add_item('Save', ?s, &method(:save))
535
- add_item('Save As', ?S, &method(:save_as))
536
- add_separator
537
- add_item('Quit', ?q, &method(:quit))
538
- title
539
- end
540
- end
541
-
542
- # This class creates the Edit pulldown menu.
543
- class EditMenu
544
- include MenuExtension
545
-
546
- # Copy data from model into primary clipboard.
547
- def copy(item)
548
- data = Editor.model2data(model.iter_first)
549
- json = JSON.pretty_generate(data, :max_nesting => false)
550
- c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
551
- c.text = json
552
- end
553
-
554
- # Copy json text from primary clipboard into model.
555
- def paste(item)
556
- c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
557
- if json = c.wait_for_text
558
- window.ask_save if @changed
559
- begin
560
- window.edit json
561
- rescue JSON::ParserError
562
- window.clear
563
- end
564
- end
565
- end
566
-
567
- # Find a string in all nodes' contents and select the found node in the
568
- # treeview.
569
- def find(item)
570
- @search = ask_for_find_term(@search) or return
571
- iter = model.get_iter('0') or return
572
- iter.recursive_each do |i|
573
- if @iter
574
- if @iter != i
575
- next
576
- else
577
- @iter = nil
578
- next
579
- end
580
- elsif @search.match(i[CONTENT_COL])
581
- set_cursor(i.path, nil, false)
582
- @iter = i
583
- break
584
- end
585
- end
586
- end
587
-
588
- # Repeat the last search given by #find.
589
- def find_again(item)
590
- @search or return
591
- iter = model.get_iter('0')
592
- iter.recursive_each do |i|
593
- if @iter
594
- if @iter != i
595
- next
596
- else
597
- @iter = nil
598
- next
599
- end
600
- elsif @search.match(i[CONTENT_COL])
601
- set_cursor(i.path, nil, false)
602
- @iter = i
603
- break
604
- end
605
- end
606
- end
607
-
608
- # Sort (Reverse sort) all elements of the selected array by the given
609
- # expression. _x_ is the element in question.
610
- def sort(item)
611
- if current = selection.selected
612
- if current.type == 'Array'
613
- parent = current.parent
614
- ary = Editor.model2data(current)
615
- order, reverse = ask_for_order
616
- order or return
617
- begin
618
- block = eval "lambda { |x| #{order} }"
619
- if reverse
620
- ary.sort! { |a,b| block[b] <=> block[a] }
621
- else
622
- ary.sort! { |a,b| block[a] <=> block[b] }
623
- end
624
- rescue => e
625
- Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
626
- else
627
- Editor.data2model(ary, model, parent) do |m|
628
- m.insert_before(parent, current)
629
- end
630
- model.remove(current)
631
- expand_collapse(parent)
632
- window.change
633
- toplevel.display_status("Array has been sorted.")
634
- end
635
- else
636
- toplevel.display_status("Only Array nodes can be sorted!")
637
- end
638
- else
639
- toplevel.display_status("Select an Array to sort first!")
640
- end
641
- end
642
-
643
- # Create the menu.
644
- def create
645
- title = MenuItem.new('Edit')
646
- title.submenu = menu
647
- add_item('Copy', ?c, &method(:copy))
648
- add_item('Paste', ?v, &method(:paste))
649
- add_separator
650
- add_item('Find', ?f, &method(:find))
651
- add_item('Find Again', ?g, &method(:find_again))
652
- add_separator
653
- add_item('Sort', ?S, &method(:sort))
654
- title
655
- end
656
- end
657
-
658
- class OptionsMenu
659
- include MenuExtension
660
-
661
- # Collapse/Expand all nodes by default.
662
- def collapsed_nodes(item)
663
- if expanded
664
- self.expanded = false
665
- collapse_all
666
- else
667
- self.expanded = true
668
- expand_all
669
- end
670
- end
671
-
672
- # Toggle pretty saving mode on/off.
673
- def pretty_saving(item)
674
- @pretty_item.toggled
675
- window.change
676
- end
677
-
678
- attr_reader :pretty_item
679
-
680
- # Create the menu.
681
- def create
682
- title = MenuItem.new('Options')
683
- title.submenu = menu
684
- add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes))
685
- @pretty_item = add_item('Pretty saving', nil, CheckMenuItem,
686
- &method(:pretty_saving))
687
- @pretty_item.active = true
688
- window.unchange
689
- title
690
- end
691
- end
692
-
693
- # This class inherits from Gtk::TreeView, to configure it and to add a lot
694
- # of behaviour to it.
695
- class JSONTreeView < Gtk::TreeView
696
- include Gtk
697
-
698
- # Creates a JSONTreeView instance, the parameter _window_ is
699
- # a MainWindow instance and used for self delegation.
700
- def initialize(window)
701
- @window = window
702
- super(TreeStore.new(Gdk::Pixbuf, String, String))
703
- self.selection.mode = SELECTION_BROWSE
704
-
705
- @expanded = false
706
- self.headers_visible = false
707
- add_columns
708
- add_popup_menu
709
- end
710
-
711
- # Returns the MainWindow instance of this JSONTreeView.
712
- attr_reader :window
713
-
714
- # Returns true, if nodes are autoexpanding, false otherwise.
715
- attr_accessor :expanded
716
-
717
- private
718
-
719
- def add_columns
720
- cell = CellRendererPixbuf.new
721
- column = TreeViewColumn.new('Icon', cell,
722
- 'pixbuf' => ICON_COL
723
- )
724
- append_column(column)
725
-
726
- cell = CellRendererText.new
727
- column = TreeViewColumn.new('Type', cell,
728
- 'text' => TYPE_COL
729
- )
730
- append_column(column)
731
-
732
- cell = CellRendererText.new
733
- cell.editable = true
734
- column = TreeViewColumn.new('Content', cell,
735
- 'text' => CONTENT_COL
736
- )
737
- cell.signal_connect(:edited, &method(:cell_edited))
738
- append_column(column)
739
- end
740
-
741
- def unify_key(iter, key)
742
- return unless iter.type == 'Key'
743
- parent = iter.parent
744
- if parent.any? { |c| c != iter and c.content == key }
745
- old_key = key
746
- i = 0
747
- begin
748
- key = sprintf("%s.%d", old_key, i += 1)
749
- end while parent.any? { |c| c != iter and c.content == key }
750
- end
751
- iter.content = key
752
- end
753
-
754
- def cell_edited(cell, path, value)
755
- iter = model.get_iter(path)
756
- case iter.type
757
- when 'Key'
758
- unify_key(iter, value)
759
- toplevel.display_status('Key has been changed.')
760
- when 'FalseClass'
761
- value.downcase!
762
- if value == 'true'
763
- iter.type, iter.content = 'TrueClass', 'true'
764
- end
765
- when 'TrueClass'
766
- value.downcase!
767
- if value == 'false'
768
- iter.type, iter.content = 'FalseClass', 'false'
769
- end
770
- when 'Numeric'
771
- iter.content =
772
- if value == 'Infinity'
773
- value
774
- else
775
- (Integer(value) rescue Float(value) rescue 0).to_s
776
- end
777
- when 'String'
778
- iter.content = value
779
- when 'Hash', 'Array'
780
- return
781
- else
782
- fail "Unknown type found in model: #{iter.type}"
783
- end
784
- window.change
785
- end
786
-
787
- def configure_value(value, type)
788
- value.editable = false
789
- case type
790
- when 'Array', 'Hash'
791
- value.text = ''
792
- when 'TrueClass'
793
- value.text = 'true'
794
- when 'FalseClass'
795
- value.text = 'false'
796
- when 'NilClass'
797
- value.text = 'null'
798
- when 'Numeric', 'String'
799
- value.text ||= ''
800
- value.editable = true
801
- else
802
- raise ArgumentError, "unknown type '#{type}' encountered"
803
- end
804
- end
805
-
806
- def add_popup_menu
807
- menu = PopUpMenu.new(self)
808
- menu.create
809
- end
810
-
811
- public
812
-
813
- # Create a _type_ node with content _content_, and add it to _parent_
814
- # in the model. If _parent_ is nil, create a new model and put it into
815
- # the editor treeview.
816
- def create_node(parent, type, content)
817
- iter = if parent
818
- model.append(parent)
819
- else
820
- new_model = Editor.data2model(nil)
821
- toplevel.view_new_model(new_model)
822
- new_model.iter_first
823
- end
824
- iter.type, iter.content = type, content
825
- expand_collapse(parent) if parent
826
- iter
827
- end
828
-
829
- # Ask for a hash key, value pair to be added to the Hash node _parent_.
830
- def ask_for_hash_pair(parent)
831
- key_input = type_input = value_input = nil
832
-
833
- dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
834
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
835
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
836
- )
837
- dialog.width_request = 640
838
-
839
- hbox = HBox.new(false, 5)
840
- hbox.pack_start(Label.new("Key:"), false)
841
- hbox.pack_start(key_input = Entry.new)
842
- key_input.text = @key || ''
843
- dialog.vbox.pack_start(hbox, false)
844
- key_input.signal_connect(:activate) do
845
- if parent.any? { |c| c.content == key_input.text }
846
- toplevel.display_status('Key already exists in Hash!')
847
- key_input.text = ''
848
- else
849
- toplevel.display_status('Key has been changed.')
850
- end
851
- end
852
-
853
- hbox = HBox.new(false, 5)
854
- hbox.pack_start(Label.new("Type:"), false)
855
- hbox.pack_start(type_input = ComboBox.new(true))
856
- ALL_TYPES.each { |t| type_input.append_text(t) }
857
- type_input.active = @type || 0
858
- dialog.vbox.pack_start(hbox, false)
859
-
860
- type_input.signal_connect(:changed) do
861
- value_input.editable = false
862
- case ALL_TYPES[type_input.active]
863
- when 'Array', 'Hash'
864
- value_input.text = ''
865
- when 'TrueClass'
866
- value_input.text = 'true'
867
- when 'FalseClass'
868
- value_input.text = 'false'
869
- when 'NilClass'
870
- value_input.text = 'null'
871
- else
872
- value_input.text = ''
873
- value_input.editable = true
874
- end
875
- end
876
-
877
- hbox = HBox.new(false, 5)
878
- hbox.pack_start(Label.new("Value:"), false)
879
- hbox.pack_start(value_input = Entry.new)
880
- value_input.width_chars = 60
881
- value_input.text = @value || ''
882
- dialog.vbox.pack_start(hbox, false)
883
-
884
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
885
- dialog.show_all
886
- self.focus = dialog
887
- dialog.run do |response|
888
- if response == Dialog::RESPONSE_ACCEPT
889
- @key = key_input.text
890
- type = ALL_TYPES[@type = type_input.active]
891
- content = value_input.text
892
- return @key, type, content
893
- end
894
- end
895
- return
896
- ensure
897
- dialog.destroy
898
- end
899
-
900
- # Ask for an element to be appended _parent_.
901
- def ask_for_element(parent = nil, default_type = nil, value_text = @content)
902
- type_input = value_input = nil
903
-
904
- dialog = Dialog.new(
905
- "New element into #{parent ? parent.type : 'root'}",
906
- nil, nil,
907
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
908
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
909
- )
910
- hbox = HBox.new(false, 5)
911
- hbox.pack_start(Label.new("Type:"), false)
912
- hbox.pack_start(type_input = ComboBox.new(true))
913
- default_active = 0
914
- types = parent ? ALL_TYPES : CONTAINER_TYPES
915
- types.each_with_index do |t, i|
916
- type_input.append_text(t)
917
- if t == default_type
918
- default_active = i
919
- end
920
- end
921
- type_input.active = default_active
922
- dialog.vbox.pack_start(hbox, false)
923
- type_input.signal_connect(:changed) do
924
- configure_value(value_input, types[type_input.active])
925
- end
926
-
927
- hbox = HBox.new(false, 5)
928
- hbox.pack_start(Label.new("Value:"), false)
929
- hbox.pack_start(value_input = Entry.new)
930
- value_input.width_chars = 60
931
- value_input.text = value_text if value_text
932
- configure_value(value_input, types[type_input.active])
933
-
934
- dialog.vbox.pack_start(hbox, false)
935
-
936
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
937
- dialog.show_all
938
- self.focus = dialog
939
- dialog.run do |response|
940
- if response == Dialog::RESPONSE_ACCEPT
941
- type = types[type_input.active]
942
- @content = case type
943
- when 'Numeric'
944
- if (t = value_input.text) == 'Infinity'
945
- 1 / 0.0
946
- else
947
- Integer(t) rescue Float(t) rescue 0
948
- end
949
- else
950
- value_input.text
951
- end.to_s
952
- return type, @content
953
- end
954
- end
955
- return
956
- ensure
957
- dialog.destroy if dialog
958
- end
959
-
960
- # Ask for an order criteria for sorting, using _x_ for the element in
961
- # question. Returns the order criterium, and true/false for reverse
962
- # sorting.
963
- def ask_for_order
964
- dialog = Dialog.new(
965
- "Give an order criterium for 'x'.",
966
- nil, nil,
967
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
968
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
969
- )
970
- hbox = HBox.new(false, 5)
971
-
972
- hbox.pack_start(Label.new("Order:"), false)
973
- hbox.pack_start(order_input = Entry.new)
974
- order_input.text = @order || 'x'
975
- order_input.width_chars = 60
976
-
977
- hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'), false)
978
-
979
- dialog.vbox.pack_start(hbox, false)
980
-
981
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
982
- dialog.show_all
983
- self.focus = dialog
984
- dialog.run do |response|
985
- if response == Dialog::RESPONSE_ACCEPT
986
- return @order = order_input.text, reverse_checkbox.active?
987
- end
988
- end
989
- return
990
- ensure
991
- dialog.destroy if dialog
992
- end
993
-
994
- # Ask for a find term to search for in the tree. Returns the term as a
995
- # string.
996
- def ask_for_find_term(search = nil)
997
- dialog = Dialog.new(
998
- "Find a node matching regex in tree.",
999
- nil, nil,
1000
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
1001
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
1002
- )
1003
- hbox = HBox.new(false, 5)
1004
-
1005
- hbox.pack_start(Label.new("Regex:"), false)
1006
- hbox.pack_start(regex_input = Entry.new)
1007
- hbox.pack_start(icase_checkbox = CheckButton.new('Icase'), false)
1008
- regex_input.width_chars = 60
1009
- if search
1010
- regex_input.text = search.source
1011
- icase_checkbox.active = search.casefold?
1012
- end
1013
-
1014
- dialog.vbox.pack_start(hbox, false)
1015
-
1016
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
1017
- dialog.show_all
1018
- self.focus = dialog
1019
- dialog.run do |response|
1020
- if response == Dialog::RESPONSE_ACCEPT
1021
- begin
1022
- return Regexp.new(regex_input.text, icase_checkbox.active? ? Regexp::IGNORECASE : 0)
1023
- rescue => e
1024
- Editor.error_dialog(self, "Evaluation of regex /#{regex_input.text}/ failed: #{e}!")
1025
- return
1026
- end
1027
- end
1028
- end
1029
- return
1030
- ensure
1031
- dialog.destroy if dialog
1032
- end
1033
-
1034
- # Expand or collapse row pointed to by _iter_ according
1035
- # to the #expanded attribute.
1036
- def expand_collapse(iter)
1037
- if expanded
1038
- expand_row(iter.path, true)
1039
- else
1040
- collapse_row(iter.path)
1041
- end
1042
- end
1043
- end
1044
-
1045
- # The editor main window
1046
- class MainWindow < Gtk::Window
1047
- include Gtk
1048
-
1049
- def initialize(encoding)
1050
- @changed = false
1051
- @encoding = encoding
1052
- super(TOPLEVEL)
1053
- display_title
1054
- set_default_size(800, 600)
1055
- signal_connect(:delete_event) { quit }
1056
-
1057
- vbox = VBox.new(false, 0)
1058
- add(vbox)
1059
- #vbox.border_width = 0
1060
-
1061
- @treeview = JSONTreeView.new(self)
1062
- @treeview.signal_connect(:'cursor-changed') do
1063
- display_status('')
1064
- end
1065
-
1066
- menu_bar = create_menu_bar
1067
- vbox.pack_start(menu_bar, false, false, 0)
1068
-
1069
- sw = ScrolledWindow.new(nil, nil)
1070
- sw.shadow_type = SHADOW_ETCHED_IN
1071
- sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
1072
- vbox.pack_start(sw, true, true, 0)
1073
- sw.add(@treeview)
1074
-
1075
- @status_bar = Statusbar.new
1076
- vbox.pack_start(@status_bar, false, false, 0)
1077
-
1078
- @filename ||= nil
1079
- if @filename
1080
- data = read_data(@filename)
1081
- view_new_model Editor.data2model(data)
1082
- end
1083
-
1084
- signal_connect(:button_release_event) do |_,event|
1085
- if event.button == 2
1086
- c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
1087
- if url = c.wait_for_text
1088
- location_open url
1089
- end
1090
- false
1091
- else
1092
- true
1093
- end
1094
- end
1095
- end
1096
-
1097
- # Creates the menu bar with the pulldown menus and returns it.
1098
- def create_menu_bar
1099
- menu_bar = MenuBar.new
1100
- @file_menu = FileMenu.new(@treeview)
1101
- menu_bar.append @file_menu.create
1102
- @edit_menu = EditMenu.new(@treeview)
1103
- menu_bar.append @edit_menu.create
1104
- @options_menu = OptionsMenu.new(@treeview)
1105
- menu_bar.append @options_menu.create
1106
- menu_bar
1107
- end
1108
-
1109
- # Sets editor status to changed, to indicate that the edited data
1110
- # containts unsaved changes.
1111
- def change
1112
- @changed = true
1113
- display_title
1114
- end
1115
-
1116
- # Sets editor status to unchanged, to indicate that the edited data
1117
- # doesn't containt unsaved changes.
1118
- def unchange
1119
- @changed = false
1120
- display_title
1121
- end
1122
-
1123
- # Puts a new model _model_ into the Gtk::TreeView to be edited.
1124
- def view_new_model(model)
1125
- @treeview.model = model
1126
- @treeview.expanded = true
1127
- @treeview.expand_all
1128
- unchange
1129
- end
1130
-
1131
- # Displays _text_ in the status bar.
1132
- def display_status(text)
1133
- @cid ||= nil
1134
- @status_bar.pop(@cid) if @cid
1135
- @cid = @status_bar.get_context_id('dummy')
1136
- @status_bar.push(@cid, text)
1137
- end
1138
-
1139
- # Opens a dialog, asking, if changes should be saved to a file.
1140
- def ask_save
1141
- if Editor.question_dialog(self,
1142
- "Unsaved changes to JSON model. Save?")
1143
- if @filename
1144
- file_save
1145
- else
1146
- file_save_as
1147
- end
1148
- end
1149
- end
1150
-
1151
- # Quit this editor, that is, leave this editor's main loop.
1152
- def quit
1153
- ask_save if @changed
1154
- if Gtk.main_level > 0
1155
- destroy
1156
- Gtk.main_quit
1157
- end
1158
- nil
1159
- end
1160
-
1161
- # Display the new title according to the editor's current state.
1162
- def display_title
1163
- title = TITLE.dup
1164
- title << ": #@filename" if @filename
1165
- title << " *" if @changed
1166
- self.title = title
1167
- end
1168
-
1169
- # Clear the current model, after asking to save all unsaved changes.
1170
- def clear
1171
- ask_save if @changed
1172
- @filename = nil
1173
- self.view_new_model nil
1174
- end
1175
-
1176
- def check_pretty_printed(json)
1177
- pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
1178
- @options_menu.pretty_item.active = pretty
1179
- end
1180
- private :check_pretty_printed
1181
-
1182
- # Open the data at the location _uri_, if given. Otherwise open a dialog
1183
- # to ask for the _uri_.
1184
- def location_open(uri = nil)
1185
- uri = ask_for_location unless uri
1186
- uri or return
1187
- ask_save if @changed
1188
- data = load_location(uri) or return
1189
- view_new_model Editor.data2model(data)
1190
- end
1191
-
1192
- # Open the file _filename_ or call the #select_file method to ask for a
1193
- # filename.
1194
- def file_open(filename = nil)
1195
- filename = select_file('Open as a JSON file') unless filename
1196
- data = load_file(filename) or return
1197
- view_new_model Editor.data2model(data)
1198
- end
1199
-
1200
- # Edit the string _json_ in the editor.
1201
- def edit(json)
1202
- if json.respond_to? :read
1203
- json = json.read
1204
- end
1205
- data = parse_json json
1206
- view_new_model Editor.data2model(data)
1207
- end
1208
-
1209
- # Save the current file.
1210
- def file_save
1211
- if @filename
1212
- store_file(@filename)
1213
- else
1214
- file_save_as
1215
- end
1216
- end
1217
-
1218
- # Save the current file as the filename
1219
- def file_save_as
1220
- filename = select_file('Save as a JSON file')
1221
- store_file(filename)
1222
- end
1223
-
1224
- # Store the current JSON document to _path_.
1225
- def store_file(path)
1226
- if path
1227
- data = Editor.model2data(@treeview.model.iter_first)
1228
- File.open(path + '.tmp', 'wb') do |output|
1229
- data or break
1230
- if @options_menu.pretty_item.active?
1231
- output.puts JSON.pretty_generate(data, :max_nesting => false)
1232
- else
1233
- output.write JSON.generate(data, :max_nesting => false)
1234
- end
1235
- end
1236
- File.rename path + '.tmp', path
1237
- @filename = path
1238
- toplevel.display_status("Saved data to '#@filename'.")
1239
- unchange
1240
- end
1241
- rescue SystemCallError => e
1242
- Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
1243
- end
1244
-
1245
- # Load the file named _filename_ into the editor as a JSON document.
1246
- def load_file(filename)
1247
- if filename
1248
- if File.directory?(filename)
1249
- Editor.error_dialog(self, "Try to select a JSON file!")
1250
- nil
1251
- else
1252
- @filename = filename
1253
- if data = read_data(filename)
1254
- toplevel.display_status("Loaded data from '#@filename'.")
1255
- end
1256
- display_title
1257
- data
1258
- end
1259
- end
1260
- end
1261
-
1262
- # Load the data at location _uri_ into the editor as a JSON document.
1263
- def load_location(uri)
1264
- data = read_data(uri) or return
1265
- @filename = nil
1266
- toplevel.display_status("Loaded data from '#{uri}'.")
1267
- display_title
1268
- data
1269
- end
1270
-
1271
- def parse_json(json)
1272
- check_pretty_printed(json)
1273
- if @encoding && !/^utf8$/i.match(@encoding)
1274
- json = JSON.iconv 'utf-8', @encoding, json
1275
- end
1276
- JSON::parse(json, :max_nesting => false, :create_additions => false)
1277
- end
1278
- private :parse_json
1279
-
1280
- # Read a JSON document from the file named _filename_, parse it into a
1281
- # ruby data structure, and return the data.
1282
- def read_data(filename)
1283
- open(filename) do |f|
1284
- json = f.read
1285
- return parse_json(json)
1286
- end
1287
- rescue => e
1288
- Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
1289
- return
1290
- end
1291
-
1292
- # Open a file selecton dialog, displaying _message_, and return the
1293
- # selected filename or nil, if no file was selected.
1294
- def select_file(message)
1295
- filename = nil
1296
- fs = FileSelection.new(message)
1297
- fs.set_modal(true)
1298
- @default_dir = File.join(Dir.pwd, '') unless @default_dir
1299
- fs.set_filename(@default_dir)
1300
- fs.set_transient_for(self)
1301
- fs.signal_connect(:destroy) { Gtk.main_quit }
1302
- fs.ok_button.signal_connect(:clicked) do
1303
- filename = fs.filename
1304
- @default_dir = File.join(File.dirname(filename), '')
1305
- fs.destroy
1306
- Gtk.main_quit
1307
- end
1308
- fs.cancel_button.signal_connect(:clicked) do
1309
- fs.destroy
1310
- Gtk.main_quit
1311
- end
1312
- fs.show_all
1313
- Gtk.main
1314
- filename
1315
- end
1316
-
1317
- # Ask for location URI a to load data from. Returns the URI as a string.
1318
- def ask_for_location
1319
- dialog = Dialog.new(
1320
- "Load data from location...",
1321
- nil, nil,
1322
- [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
1323
- [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
1324
- )
1325
- hbox = HBox.new(false, 5)
1326
-
1327
- hbox.pack_start(Label.new("Location:"), false)
1328
- hbox.pack_start(location_input = Entry.new)
1329
- location_input.width_chars = 60
1330
- location_input.text = @location || ''
1331
-
1332
- dialog.vbox.pack_start(hbox, false)
1333
-
1334
- dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
1335
- dialog.show_all
1336
- dialog.run do |response|
1337
- if response == Dialog::RESPONSE_ACCEPT
1338
- return @location = location_input.text
1339
- end
1340
- end
1341
- return
1342
- ensure
1343
- dialog.destroy if dialog
1344
- end
1345
- end
1346
-
1347
- class << self
1348
- # Starts a JSON Editor. If a block was given, it yields
1349
- # to the JSON::Editor::MainWindow instance.
1350
- def start(encoding = 'utf8') # :yield: window
1351
- Gtk.init
1352
- @window = Editor::MainWindow.new(encoding)
1353
- @window.icon_list = [ Editor.fetch_icon('json') ]
1354
- yield @window if block_given?
1355
- @window.show_all
1356
- Gtk.main
1357
- end
1358
-
1359
- # Edit the string _json_ with encoding _encoding_ in the editor.
1360
- def edit(json, encoding = 'utf8')
1361
- start(encoding) do |window|
1362
- window.edit json
1363
- end
1364
- end
1365
-
1366
- attr_reader :window
1367
- end
1368
- end
1369
- end