json 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of json might be problematic. Click here for more details.
- data/CHANGES +13 -2
- data/README +3 -2
- data/RUBY +58 -0
- data/Rakefile +4 -3
- data/VERSION +1 -1
- data/ext/json/ext/generator/unicode.c +1 -1
- data/ext/json/ext/parser/parser.c +146 -99
- data/ext/json/ext/parser/parser.rl +70 -16
- data/lib/json.rb +2 -3
- data/lib/json/common.rb +11 -2
- data/lib/json/editor.rb +1295 -1207
- data/lib/json/pure/parser.rb +35 -8
- data/lib/json/version.rb +1 -1
- data/tests/fixtures/{pass18.json → fail18.json} +0 -0
- data/tests/fixtures/{fail15.json → pass15.json} +0 -0
- data/tests/fixtures/{fail16.json → pass16.json} +0 -0
- data/tests/fixtures/{fail17.json → pass17.json} +0 -0
- data/tests/fixtures/{fail26.json → pass26.json} +0 -0
- data/tests/test_json.rb +19 -0
- data/tests/test_json_fixtures.rb +1 -1
- data/tools/fuzz.rb +1 -1
- metadata +8 -7
@@ -2,17 +2,14 @@
|
|
2
2
|
|
3
3
|
#include "ruby.h"
|
4
4
|
#include "re.h"
|
5
|
+
#include "st.h"
|
5
6
|
#include "unicode.h"
|
6
7
|
|
7
|
-
#ifndef swap16
|
8
|
-
#define swap16(x) ((((x)&0xFF)<<8) | (((x)>>8)&0xFF))
|
9
|
-
#endif
|
10
|
-
|
11
8
|
#define EVIL 0x666
|
12
9
|
|
13
|
-
static VALUE mJSON, mExt, cParser, eParserError;
|
10
|
+
static VALUE mJSON, mExt, cParser, eParserError, eNestingError;
|
14
11
|
|
15
|
-
static ID i_json_creatable_p, i_json_create, i_create_id, i_chr;
|
12
|
+
static ID i_json_creatable_p, i_json_create, i_create_id, i_chr, i_max_nesting;
|
16
13
|
|
17
14
|
typedef struct JSON_ParserStruct {
|
18
15
|
VALUE Vsource;
|
@@ -20,6 +17,8 @@ typedef struct JSON_ParserStruct {
|
|
20
17
|
long len;
|
21
18
|
char *memo;
|
22
19
|
VALUE create_id;
|
20
|
+
int max_nesting;
|
21
|
+
int current_nesting;
|
23
22
|
} JSON_Parser;
|
24
23
|
|
25
24
|
static char *JSON_parse_object(JSON_Parser *json, char *p, char *pe, VALUE *result);
|
@@ -95,6 +94,11 @@ static char *JSON_parse_object(JSON_Parser *json, char *p, char *pe, VALUE *resu
|
|
95
94
|
{
|
96
95
|
int cs = EVIL;
|
97
96
|
VALUE last_name = Qnil;
|
97
|
+
|
98
|
+
if (json->max_nesting && json->current_nesting > json->max_nesting) {
|
99
|
+
rb_raise(eNestingError, "nesting of %d is to deep", json->current_nesting);
|
100
|
+
}
|
101
|
+
|
98
102
|
*result = rb_hash_new();
|
99
103
|
|
100
104
|
%% write init;
|
@@ -144,12 +148,18 @@ static char *JSON_parse_object(JSON_Parser *json, char *p, char *pe, VALUE *resu
|
|
144
148
|
}
|
145
149
|
|
146
150
|
action parse_array {
|
147
|
-
char *np
|
151
|
+
char *np;
|
152
|
+
json->current_nesting += 1;
|
153
|
+
np = JSON_parse_array(json, fpc, pe, result);
|
154
|
+
json->current_nesting -= 1;
|
148
155
|
if (np == NULL) fbreak; else fexec np;
|
149
156
|
}
|
150
157
|
|
151
158
|
action parse_object {
|
152
|
-
char *np
|
159
|
+
char *np;
|
160
|
+
json->current_nesting += 1;
|
161
|
+
np = JSON_parse_object(json, fpc, pe, result);
|
162
|
+
json->current_nesting -= 1;
|
153
163
|
if (np == NULL) fbreak; else fexec np;
|
154
164
|
}
|
155
165
|
|
@@ -269,6 +279,10 @@ static char *JSON_parse_float(JSON_Parser *json, char *p, char *pe, VALUE *resul
|
|
269
279
|
static char *JSON_parse_array(JSON_Parser *json, char *p, char *pe, VALUE *result)
|
270
280
|
{
|
271
281
|
int cs = EVIL;
|
282
|
+
|
283
|
+
if (json->max_nesting && json->current_nesting > json->max_nesting) {
|
284
|
+
rb_raise(eNestingError, "nesting of %d is to deep", json->current_nesting);
|
285
|
+
}
|
272
286
|
*result = rb_ary_new();
|
273
287
|
|
274
288
|
%% write init;
|
@@ -281,7 +295,7 @@ static char *JSON_parse_array(JSON_Parser *json, char *p, char *pe, VALUE *resul
|
|
281
295
|
}
|
282
296
|
}
|
283
297
|
|
284
|
-
static VALUE
|
298
|
+
static VALUE json_string_unescape(char *p, char *pe)
|
285
299
|
{
|
286
300
|
VALUE result = rb_str_buf_new(pe - p + 1);
|
287
301
|
|
@@ -322,6 +336,10 @@ static VALUE json_string_escape(char *p, char *pe)
|
|
322
336
|
p = JSON_convert_UTF16_to_UTF8(result, p, pe, strictConversion);
|
323
337
|
}
|
324
338
|
break;
|
339
|
+
default:
|
340
|
+
rb_str_buf_cat(result, p, 1);
|
341
|
+
p++;
|
342
|
+
break;
|
325
343
|
}
|
326
344
|
} else {
|
327
345
|
char *q = p;
|
@@ -340,13 +358,13 @@ static VALUE json_string_escape(char *p, char *pe)
|
|
340
358
|
write data;
|
341
359
|
|
342
360
|
action parse_string {
|
343
|
-
*result =
|
361
|
+
*result = json_string_unescape(json->memo + 1, p);
|
344
362
|
if (NIL_P(*result)) fbreak; else fexec p + 1;
|
345
363
|
}
|
346
364
|
|
347
365
|
action exit { fbreak; }
|
348
366
|
|
349
|
-
main := '"' ((^(["\\] | 0..0x1f) | '\\'["\\/bfnrt] | '\\u'[0-9a-fA-F]{4})* %parse_string) '"' @exit;
|
367
|
+
main := '"' ((^(["\\] | 0..0x1f) | '\\'["\\/bfnrt] | '\\u'[0-9a-fA-F]{4} | '\\'^(["\\/bfnrtu]|0..0x1f))* %parse_string) '"' @exit;
|
350
368
|
}%%
|
351
369
|
|
352
370
|
static char *JSON_parse_string(JSON_Parser *json, char *p, char *pe, VALUE *result)
|
@@ -374,12 +392,16 @@ static char *JSON_parse_string(JSON_Parser *json, char *p, char *pe, VALUE *resu
|
|
374
392
|
include JSON_common;
|
375
393
|
|
376
394
|
action parse_object {
|
377
|
-
char *np
|
395
|
+
char *np;
|
396
|
+
json->current_nesting = 1;
|
397
|
+
np = JSON_parse_object(json, fpc, pe, &result);
|
378
398
|
if (np == NULL) fbreak; else fexec np;
|
379
399
|
}
|
380
400
|
|
381
401
|
action parse_array {
|
382
|
-
char *np
|
402
|
+
char *np;
|
403
|
+
json->current_nesting = 1;
|
404
|
+
np = JSON_parse_array(json, fpc, pe, &result);
|
383
405
|
if (np == NULL) fbreak; else fexec np;
|
384
406
|
}
|
385
407
|
|
@@ -402,21 +424,51 @@ static char *JSON_parse_string(JSON_Parser *json, char *p, char *pe, VALUE *resu
|
|
402
424
|
*/
|
403
425
|
|
404
426
|
/*
|
405
|
-
* call-seq: new(source)
|
427
|
+
* call-seq: new(source, opts => {})
|
406
428
|
*
|
407
429
|
* Creates a new JSON::Ext::Parser instance for the string _source_.
|
430
|
+
*
|
431
|
+
* Creates a new JSON::Ext::Parser instance for the string _source_.
|
432
|
+
*
|
433
|
+
* It will be configured by the _opts_ hash. _opts_ can have the following
|
434
|
+
* keys:
|
435
|
+
*
|
436
|
+
* _opts_ can have the following keys:
|
437
|
+
* * *max_nesting*: The maximum depth of nesting allowed in the parsed data
|
438
|
+
* structures. Disable depth checking with :max_nesting => false.
|
408
439
|
*/
|
409
|
-
static VALUE cParser_initialize(VALUE
|
440
|
+
static VALUE cParser_initialize(int argc, VALUE *argv, VALUE self)
|
410
441
|
{
|
411
442
|
char *ptr;
|
412
443
|
long len;
|
444
|
+
VALUE source, opts;
|
413
445
|
GET_STRUCT;
|
446
|
+
rb_scan_args(argc, argv, "11", &source, &opts);
|
414
447
|
source = StringValue(source);
|
415
448
|
ptr = RSTRING_PTR(source);
|
416
449
|
len = RSTRING_LEN(source);
|
417
450
|
if (len < 2) {
|
418
451
|
rb_raise(eParserError, "A JSON text must at least contain two octets!");
|
419
452
|
}
|
453
|
+
json->max_nesting = 19;
|
454
|
+
if (!NIL_P(opts)) {
|
455
|
+
opts = rb_convert_type(opts, T_HASH, "Hash", "to_hash");
|
456
|
+
if (NIL_P(opts)) {
|
457
|
+
rb_raise(rb_eArgError, "opts needs to be like a hash");
|
458
|
+
} else {
|
459
|
+
VALUE s_max_nesting = ID2SYM(i_max_nesting);
|
460
|
+
if (st_lookup(RHASH(opts)->tbl, s_max_nesting, 0)) {
|
461
|
+
VALUE max_nesting = rb_hash_aref(opts, s_max_nesting);
|
462
|
+
if (RTEST(max_nesting)) {
|
463
|
+
Check_Type(max_nesting, T_FIXNUM);
|
464
|
+
json->max_nesting = FIX2INT(max_nesting);
|
465
|
+
} else {
|
466
|
+
json->max_nesting = 0;
|
467
|
+
}
|
468
|
+
}
|
469
|
+
}
|
470
|
+
}
|
471
|
+
json->current_nesting = 0;
|
420
472
|
/*
|
421
473
|
Convert these?
|
422
474
|
if (len >= 4 && ptr[0] == 0 && ptr[1] == 0 && ptr[2] == 0) {
|
@@ -503,8 +555,9 @@ void Init_parser()
|
|
503
555
|
mExt = rb_define_module_under(mJSON, "Ext");
|
504
556
|
cParser = rb_define_class_under(mExt, "Parser", rb_cObject);
|
505
557
|
eParserError = rb_path2class("JSON::ParserError");
|
558
|
+
eNestingError = rb_path2class("JSON::NestingError");
|
506
559
|
rb_define_alloc_func(cParser, cJSON_parser_s_allocate);
|
507
|
-
rb_define_method(cParser, "initialize", cParser_initialize, 1);
|
560
|
+
rb_define_method(cParser, "initialize", cParser_initialize, -1);
|
508
561
|
rb_define_method(cParser, "parse", cParser_parse, 0);
|
509
562
|
rb_define_method(cParser, "source", cParser_source, 0);
|
510
563
|
|
@@ -512,4 +565,5 @@ void Init_parser()
|
|
512
565
|
i_json_create = rb_intern("json_create");
|
513
566
|
i_create_id = rb_intern("create_id");
|
514
567
|
i_chr = rb_intern("chr");
|
568
|
+
i_max_nesting = rb_intern("max_nesting");
|
515
569
|
}
|
data/lib/json.rb
CHANGED
@@ -34,9 +34,8 @@ require 'json/common'
|
|
34
34
|
#
|
35
35
|
# == License
|
36
36
|
#
|
37
|
-
# This is
|
38
|
-
#
|
39
|
-
# Software Foundation: www.gnu.org/copyleft/gpl.html
|
37
|
+
# This software is distributed under the same license as Ruby itself, see
|
38
|
+
# http://www.ruby-lang.org/en/LICENSE.txt.
|
40
39
|
#
|
41
40
|
# == Download
|
42
41
|
#
|
data/lib/json/common.rb
CHANGED
@@ -77,6 +77,10 @@ module JSON
|
|
77
77
|
# This exception is raised, if a parser error occurs.
|
78
78
|
class ParserError < JSONError; end
|
79
79
|
|
80
|
+
# This exception is raised, if the nesting of parsed datastructures is too
|
81
|
+
# deep.
|
82
|
+
class NestingError < ParserError; end
|
83
|
+
|
80
84
|
# This exception is raised, if a generator or unparser error occurs.
|
81
85
|
class GeneratorError < JSONError; end
|
82
86
|
# For backwards compatibility
|
@@ -93,8 +97,13 @@ module JSON
|
|
93
97
|
module_function
|
94
98
|
|
95
99
|
# Parse the JSON string _source_ into a Ruby data structure and return it.
|
96
|
-
|
97
|
-
|
100
|
+
#
|
101
|
+
# _opts_ can have the following
|
102
|
+
# keys:
|
103
|
+
# * *max_nesting*: The maximum depth of nesting allowed in the parsed data
|
104
|
+
# structures. Disable depth checking with :max_nesting => false.
|
105
|
+
def parse(source, opts = {})
|
106
|
+
JSON.parser.new(source, opts).parse
|
98
107
|
end
|
99
108
|
|
100
109
|
# Unparse the Ruby data structure _obj_ into a single line JSON string and
|
data/lib/json/editor.rb
CHANGED
@@ -1,1207 +1,1295 @@
|
|
1
|
-
# To use the GUI JSON editor, start the edit_json.rb executable script. It
|
2
|
-
# requires ruby-gtk to be installed.
|
3
|
-
|
4
|
-
require 'gtk2'
|
5
|
-
require 'iconv'
|
6
|
-
require 'json'
|
7
|
-
require 'rbconfig'
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
#
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
dialog.
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
dialog = MessageDialog.new(window, Dialog::MODAL,
|
59
|
-
MessageDialog::
|
60
|
-
MessageDialog::
|
61
|
-
dialog.
|
62
|
-
|
63
|
-
|
64
|
-
ensure
|
65
|
-
dialog.destroy if dialog
|
66
|
-
end
|
67
|
-
|
68
|
-
#
|
69
|
-
#
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
when '
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
iter.
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
end
|
167
|
-
|
168
|
-
#
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
end
|
184
|
-
|
185
|
-
#
|
186
|
-
def
|
187
|
-
self[
|
188
|
-
end
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
#
|
198
|
-
def
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
#
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
#
|
227
|
-
def
|
228
|
-
|
229
|
-
end
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
end
|
348
|
-
|
349
|
-
toplevel.display_status(
|
350
|
-
"
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
"
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
type, content = ask_for_element(parent)
|
405
|
-
type or return
|
406
|
-
iter =
|
407
|
-
|
408
|
-
toplevel.display_status("
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
end
|
418
|
-
end
|
419
|
-
|
420
|
-
#
|
421
|
-
def
|
422
|
-
if current = selection.selected
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
end
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
end
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
#
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
add_item('
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
@
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
#
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
when '
|
747
|
-
value.
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
hbox.
|
858
|
-
hbox.
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
)
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
dialog.
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
dialog.
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
add(
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
@
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
@
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
@
|
1039
|
-
@
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
end
|
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
|
+
require 'open-uri'
|
9
|
+
|
10
|
+
module JSON
|
11
|
+
module Editor
|
12
|
+
include Gtk
|
13
|
+
|
14
|
+
# Beginning of the editor window title
|
15
|
+
TITLE = 'JSON Editor'.freeze
|
16
|
+
|
17
|
+
# Columns constants
|
18
|
+
ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2
|
19
|
+
|
20
|
+
# JSON primitive types (Containers)
|
21
|
+
CONTAINER_TYPES = %w[Array Hash].sort
|
22
|
+
# All JSON primitive types
|
23
|
+
ALL_TYPES = (%w[TrueClass FalseClass Numeric String NilClass] +
|
24
|
+
CONTAINER_TYPES).sort
|
25
|
+
|
26
|
+
# The Nodes necessary for the tree representation of a JSON document
|
27
|
+
ALL_NODES = (ALL_TYPES + %w[Key]).sort
|
28
|
+
|
29
|
+
DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event|
|
30
|
+
case event.keyval
|
31
|
+
when Gdk::Keyval::GDK_Return
|
32
|
+
dialog.response Dialog::RESPONSE_ACCEPT
|
33
|
+
when Gdk::Keyval::GDK_Escape
|
34
|
+
dialog.response Dialog::RESPONSE_REJECT
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
|
39
|
+
def Editor.fetch_icon(name)
|
40
|
+
@icon_cache ||= {}
|
41
|
+
unless @icon_cache.key?(name)
|
42
|
+
path = File.dirname(__FILE__)
|
43
|
+
@icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
|
44
|
+
end
|
45
|
+
@icon_cache[name]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Opens an error dialog on top of _window_ showing the error message
|
49
|
+
# _text_.
|
50
|
+
def Editor.error_dialog(window, text)
|
51
|
+
dialog = MessageDialog.new(window, Dialog::MODAL,
|
52
|
+
MessageDialog::ERROR,
|
53
|
+
MessageDialog::BUTTONS_CLOSE, text)
|
54
|
+
dialog.show_all
|
55
|
+
window.focus = dialog
|
56
|
+
dialog.run
|
57
|
+
rescue TypeError
|
58
|
+
dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
|
59
|
+
MessageDialog::ERROR,
|
60
|
+
MessageDialog::BUTTONS_CLOSE, text)
|
61
|
+
dialog.show_all
|
62
|
+
window.focus = dialog
|
63
|
+
dialog.run
|
64
|
+
ensure
|
65
|
+
dialog.destroy if dialog
|
66
|
+
end
|
67
|
+
|
68
|
+
# Opens a yes/no question dialog on top of _window_ showing the error
|
69
|
+
# message _text_. If yes was answered _true_ is returned, otherwise
|
70
|
+
# _false_.
|
71
|
+
def Editor.question_dialog(window, text)
|
72
|
+
dialog = MessageDialog.new(window, Dialog::MODAL,
|
73
|
+
MessageDialog::QUESTION,
|
74
|
+
MessageDialog::BUTTONS_YES_NO, text)
|
75
|
+
dialog.show_all
|
76
|
+
window.focus = dialog
|
77
|
+
dialog.run do |response|
|
78
|
+
return Gtk::Dialog::RESPONSE_YES === response
|
79
|
+
end
|
80
|
+
ensure
|
81
|
+
dialog.destroy if dialog
|
82
|
+
end
|
83
|
+
|
84
|
+
# Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
|
85
|
+
# data structure and return it.
|
86
|
+
def Editor.model2data(iter)
|
87
|
+
return nil if iter.nil?
|
88
|
+
case iter.type
|
89
|
+
when 'Hash'
|
90
|
+
hash = {}
|
91
|
+
iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
|
92
|
+
hash
|
93
|
+
when 'Array'
|
94
|
+
array = Array.new(iter.n_children)
|
95
|
+
iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
|
96
|
+
array
|
97
|
+
when 'Key'
|
98
|
+
iter.content
|
99
|
+
when 'String'
|
100
|
+
iter.content
|
101
|
+
when 'Numeric'
|
102
|
+
content = iter.content
|
103
|
+
if /\./.match(content)
|
104
|
+
content.to_f
|
105
|
+
else
|
106
|
+
content.to_i
|
107
|
+
end
|
108
|
+
when 'TrueClass'
|
109
|
+
true
|
110
|
+
when 'FalseClass'
|
111
|
+
false
|
112
|
+
when 'NilClass'
|
113
|
+
nil
|
114
|
+
else
|
115
|
+
fail "Unknown type found in model: #{iter.type}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Convert the Ruby data structure _data_ into tree model data for Gtk and
|
120
|
+
# returns the whole model. If the parameter _model_ wasn't given a new
|
121
|
+
# Gtk::TreeStore is created as the model. The _parent_ parameter specifies
|
122
|
+
# the parent node (iter, Gtk:TreeIter instance) to which the data is
|
123
|
+
# appended, alternativeley the result of the yielded block is used as iter.
|
124
|
+
def Editor.data2model(data, model = nil, parent = nil)
|
125
|
+
model ||= TreeStore.new(Gdk::Pixbuf, String, String)
|
126
|
+
iter = if block_given?
|
127
|
+
yield model
|
128
|
+
else
|
129
|
+
model.append(parent)
|
130
|
+
end
|
131
|
+
case data
|
132
|
+
when Hash
|
133
|
+
iter.type = 'Hash'
|
134
|
+
data.sort.each do |key, value|
|
135
|
+
pair_iter = model.append(iter)
|
136
|
+
pair_iter.type = 'Key'
|
137
|
+
pair_iter.content = key.to_s
|
138
|
+
Editor.data2model(value, model, pair_iter)
|
139
|
+
end
|
140
|
+
when Array
|
141
|
+
iter.type = 'Array'
|
142
|
+
data.each do |value|
|
143
|
+
Editor.data2model(value, model, iter)
|
144
|
+
end
|
145
|
+
when Numeric
|
146
|
+
iter.type = 'Numeric'
|
147
|
+
iter.content = data.to_s
|
148
|
+
when String, true, false, nil
|
149
|
+
iter.type = data.class.name
|
150
|
+
iter.content = data.nil? ? 'null' : data.to_s
|
151
|
+
else
|
152
|
+
iter.type = 'String'
|
153
|
+
iter.content = data.to_s
|
154
|
+
end
|
155
|
+
model
|
156
|
+
end
|
157
|
+
|
158
|
+
# The Gtk::TreeIter class is reopened and some auxiliary methods are added.
|
159
|
+
class Gtk::TreeIter
|
160
|
+
include Enumerable
|
161
|
+
|
162
|
+
# Traverse each of this Gtk::TreeIter instance's children
|
163
|
+
# and yield to them.
|
164
|
+
def each
|
165
|
+
n_children.times { |i| yield nth_child(i) }
|
166
|
+
end
|
167
|
+
|
168
|
+
# Recursively traverse all nodes of this Gtk::TreeIter's subtree
|
169
|
+
# (including self) and yield to them.
|
170
|
+
def recursive_each(&block)
|
171
|
+
yield self
|
172
|
+
each do |i|
|
173
|
+
i.recursive_each(&block)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Remove the subtree of this Gtk::TreeIter instance from the
|
178
|
+
# model _model_.
|
179
|
+
def remove_subtree(model)
|
180
|
+
while current = first_child
|
181
|
+
model.remove(current)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the type of this node.
|
186
|
+
def type
|
187
|
+
self[TYPE_COL]
|
188
|
+
end
|
189
|
+
|
190
|
+
# Sets the type of this node to _value_. This implies setting
|
191
|
+
# the respective icon accordingly.
|
192
|
+
def type=(value)
|
193
|
+
self[TYPE_COL] = value
|
194
|
+
self[ICON_COL] = Editor.fetch_icon(value)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Returns the content of this node.
|
198
|
+
def content
|
199
|
+
self[CONTENT_COL]
|
200
|
+
end
|
201
|
+
|
202
|
+
# Sets the content of this node to _value_.
|
203
|
+
def content=(value)
|
204
|
+
self[CONTENT_COL] = value
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# This module bundles some method, that can be used to create a menu. It
|
209
|
+
# should be included into the class in question.
|
210
|
+
module MenuExtension
|
211
|
+
include Gtk
|
212
|
+
|
213
|
+
# Creates a Menu, that includes MenuExtension. _treeview_ is the
|
214
|
+
# Gtk::TreeView, on which it operates.
|
215
|
+
def initialize(treeview)
|
216
|
+
@treeview = treeview
|
217
|
+
@menu = Menu.new
|
218
|
+
end
|
219
|
+
|
220
|
+
# Returns the Gtk::TreeView of this menu.
|
221
|
+
attr_reader :treeview
|
222
|
+
|
223
|
+
# Returns the menu.
|
224
|
+
attr_reader :menu
|
225
|
+
|
226
|
+
# Adds a Gtk::SeparatorMenuItem to this instance's #menu.
|
227
|
+
def add_separator
|
228
|
+
menu.append SeparatorMenuItem.new
|
229
|
+
end
|
230
|
+
|
231
|
+
# Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
|
232
|
+
# string, _klass_ is the item type, and _callback_ is the procedure, that
|
233
|
+
# is called if the _item_ is activated.
|
234
|
+
def add_item(label, keyval = nil, klass = MenuItem, &callback)
|
235
|
+
label = "#{label} (C-#{keyval.chr})" if keyval
|
236
|
+
item = klass.new(label)
|
237
|
+
item.signal_connect(:activate, &callback)
|
238
|
+
if keyval
|
239
|
+
self.signal_connect(:'key-press-event') do |item, event|
|
240
|
+
if event.state & Gdk::Window::ModifierType::CONTROL_MASK != 0 and
|
241
|
+
event.keyval == keyval
|
242
|
+
callback.call item
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
menu.append item
|
247
|
+
item
|
248
|
+
end
|
249
|
+
|
250
|
+
# This method should be implemented in subclasses to create the #menu of
|
251
|
+
# this instance. It has to be called after an instance of this class is
|
252
|
+
# created, to build the menu.
|
253
|
+
def create
|
254
|
+
raise NotImplementedError
|
255
|
+
end
|
256
|
+
|
257
|
+
def method_missing(*a, &b)
|
258
|
+
treeview.__send__(*a, &b)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# This class creates the popup menu, that opens when clicking onto the
|
263
|
+
# treeview.
|
264
|
+
class PopUpMenu
|
265
|
+
include MenuExtension
|
266
|
+
|
267
|
+
# Change the type or content of the selected node.
|
268
|
+
def change_node(item)
|
269
|
+
if current = selection.selected
|
270
|
+
parent = current.parent
|
271
|
+
old_type, old_content = current.type, current.content
|
272
|
+
if ALL_TYPES.include?(old_type)
|
273
|
+
@clipboard_data = Editor.model2data(current)
|
274
|
+
type, content = ask_for_element(parent, current.type,
|
275
|
+
current.content)
|
276
|
+
if type
|
277
|
+
current.type, current.content = type, content
|
278
|
+
current.remove_subtree(model)
|
279
|
+
toplevel.display_status("Changed a node in tree.")
|
280
|
+
window.change
|
281
|
+
end
|
282
|
+
else
|
283
|
+
toplevel.display_status(
|
284
|
+
"Cannot change node of type #{old_type} in tree!")
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Cut the selected node and its subtree, and save it into the
|
290
|
+
# clipboard.
|
291
|
+
def cut_node(item)
|
292
|
+
if current = selection.selected
|
293
|
+
if current and current.type == 'Key'
|
294
|
+
@clipboard_data = {
|
295
|
+
current.content => Editor.model2data(current.first_child)
|
296
|
+
}
|
297
|
+
else
|
298
|
+
@clipboard_data = Editor.model2data(current)
|
299
|
+
end
|
300
|
+
model.remove(current)
|
301
|
+
window.change
|
302
|
+
toplevel.display_status("Cut a node from tree.")
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Copy the selected node and its subtree, and save it into the
|
307
|
+
# clipboard.
|
308
|
+
def copy_node(item)
|
309
|
+
if current = selection.selected
|
310
|
+
if current and current.type == 'Key'
|
311
|
+
@clipboard_data = {
|
312
|
+
current.content => Editor.model2data(current.first_child)
|
313
|
+
}
|
314
|
+
else
|
315
|
+
@clipboard_data = Editor.model2data(current)
|
316
|
+
end
|
317
|
+
window.change
|
318
|
+
toplevel.display_status("Copied a node from tree.")
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# Paste the data in the clipboard into the selected Array or Hash by
|
323
|
+
# appending it.
|
324
|
+
def paste_node_appending(item)
|
325
|
+
if current = selection.selected
|
326
|
+
if @clipboard_data
|
327
|
+
case current.type
|
328
|
+
when 'Array'
|
329
|
+
Editor.data2model(@clipboard_data, model, current)
|
330
|
+
expand_collapse(current)
|
331
|
+
when 'Hash'
|
332
|
+
if @clipboard_data.is_a? Hash
|
333
|
+
parent = current.parent
|
334
|
+
hash = Editor.model2data(current)
|
335
|
+
model.remove(current)
|
336
|
+
hash.update(@clipboard_data)
|
337
|
+
Editor.data2model(hash, model, parent)
|
338
|
+
if parent
|
339
|
+
expand_collapse(parent)
|
340
|
+
elsif @expanded
|
341
|
+
expand_all
|
342
|
+
end
|
343
|
+
window.change
|
344
|
+
else
|
345
|
+
toplevel.display_status(
|
346
|
+
"Cannot paste non-#{current.type} data into '#{current.type}'!")
|
347
|
+
end
|
348
|
+
else
|
349
|
+
toplevel.display_status(
|
350
|
+
"Cannot paste node below '#{current.type}'!")
|
351
|
+
end
|
352
|
+
else
|
353
|
+
toplevel.display_status("Nothing to paste in clipboard!")
|
354
|
+
end
|
355
|
+
else
|
356
|
+
toplevel.display_status("Append a node into the root first!")
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# Paste the data in the clipboard into the selected Array inserting it
|
361
|
+
# before the selected element.
|
362
|
+
def paste_node_inserting_before(item)
|
363
|
+
if current = selection.selected
|
364
|
+
if @clipboard_data
|
365
|
+
parent = current.parent or return
|
366
|
+
parent_type = parent.type
|
367
|
+
if parent_type == 'Array'
|
368
|
+
selected_index = parent.each_with_index do |c, i|
|
369
|
+
break i if c == current
|
370
|
+
end
|
371
|
+
Editor.data2model(@clipboard_data, model, parent) do |m|
|
372
|
+
m.insert_before(parent, current)
|
373
|
+
end
|
374
|
+
expand_collapse(current)
|
375
|
+
toplevel.display_status("Inserted an element to " +
|
376
|
+
"'#{parent_type}' before index #{selected_index}.")
|
377
|
+
window.change
|
378
|
+
else
|
379
|
+
toplevel.display_status(
|
380
|
+
"Cannot insert node below '#{parent_type}'!")
|
381
|
+
end
|
382
|
+
else
|
383
|
+
toplevel.display_status("Nothing to paste in clipboard!")
|
384
|
+
end
|
385
|
+
else
|
386
|
+
toplevel.display_status("Append a node into the root first!")
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# Append a new node to the selected Hash or Array.
|
391
|
+
def append_new_node(item)
|
392
|
+
if parent = selection.selected
|
393
|
+
parent_type = parent.type
|
394
|
+
case parent_type
|
395
|
+
when 'Hash'
|
396
|
+
key, type, content = ask_for_hash_pair(parent)
|
397
|
+
key or return
|
398
|
+
iter = create_node(parent, 'Key', key)
|
399
|
+
iter = create_node(iter, type, content)
|
400
|
+
toplevel.display_status(
|
401
|
+
"Added a (key, value)-pair to '#{parent_type}'.")
|
402
|
+
window.change
|
403
|
+
when 'Array'
|
404
|
+
type, content = ask_for_element(parent)
|
405
|
+
type or return
|
406
|
+
iter = create_node(parent, type, content)
|
407
|
+
window.change
|
408
|
+
toplevel.display_status("Appendend an element to '#{parent_type}'.")
|
409
|
+
else
|
410
|
+
toplevel.display_status("Cannot append to '#{parent_type}'!")
|
411
|
+
end
|
412
|
+
else
|
413
|
+
type, content = ask_for_element
|
414
|
+
type or return
|
415
|
+
iter = create_node(nil, type, content)
|
416
|
+
window.change
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# Insert a new node into an Array before the selected element.
|
421
|
+
def insert_new_node(item)
|
422
|
+
if current = selection.selected
|
423
|
+
parent = current.parent or return
|
424
|
+
parent_parent = parent.parent
|
425
|
+
parent_type = parent.type
|
426
|
+
if parent_type == 'Array'
|
427
|
+
selected_index = parent.each_with_index do |c, i|
|
428
|
+
break i if c == current
|
429
|
+
end
|
430
|
+
type, content = ask_for_element(parent)
|
431
|
+
type or return
|
432
|
+
iter = model.insert_before(parent, current)
|
433
|
+
iter.type, iter.content = type, content
|
434
|
+
toplevel.display_status("Inserted an element to " +
|
435
|
+
"'#{parent_type}' before index #{selected_index}.")
|
436
|
+
window.change
|
437
|
+
else
|
438
|
+
toplevel.display_status(
|
439
|
+
"Cannot insert node below '#{parent_type}'!")
|
440
|
+
end
|
441
|
+
else
|
442
|
+
toplevel.display_status("Append a node into the root first!")
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
# Recursively collapse/expand a subtree starting from the selected node.
|
447
|
+
def collapse_expand(item)
|
448
|
+
if current = selection.selected
|
449
|
+
if row_expanded?(current.path)
|
450
|
+
collapse_row(current.path)
|
451
|
+
else
|
452
|
+
expand_row(current.path, true)
|
453
|
+
end
|
454
|
+
else
|
455
|
+
toplevel.display_status("Append a node into the root first!")
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
# Create the menu.
|
460
|
+
def create
|
461
|
+
add_item("Change node", ?n, &method(:change_node))
|
462
|
+
add_separator
|
463
|
+
add_item("Cut node", ?x, &method(:cut_node))
|
464
|
+
add_item("Copy node", ?c, &method(:copy_node))
|
465
|
+
add_item("Paste node (appending)", ?v, &method(:paste_node_appending))
|
466
|
+
add_item("Paste node (inserting before)", ?V,
|
467
|
+
&method(:paste_node_inserting_before))
|
468
|
+
add_separator
|
469
|
+
add_item("Append new node", ?a, &method(:append_new_node))
|
470
|
+
add_item("Insert new node before", ?i, &method(:insert_new_node))
|
471
|
+
add_separator
|
472
|
+
add_item("Collapse/Expand node (recursively)", ?C,
|
473
|
+
&method(:collapse_expand))
|
474
|
+
|
475
|
+
menu.show_all
|
476
|
+
signal_connect(:button_press_event) do |widget, event|
|
477
|
+
if event.kind_of? Gdk::EventButton and event.button == 3
|
478
|
+
menu.popup(nil, nil, event.button, event.time)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
signal_connect(:popup_menu) do
|
482
|
+
menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
# This class creates the File pulldown menu.
|
488
|
+
class FileMenu
|
489
|
+
include MenuExtension
|
490
|
+
|
491
|
+
# Clear the model and filename, but ask to save the JSON document, if
|
492
|
+
# unsaved changes have occured.
|
493
|
+
def new(item)
|
494
|
+
window.clear
|
495
|
+
end
|
496
|
+
|
497
|
+
# Open a file and load it into the editor. Ask to save the JSON document
|
498
|
+
# first, if unsaved changes have occured.
|
499
|
+
def open(item)
|
500
|
+
window.file_open
|
501
|
+
end
|
502
|
+
|
503
|
+
def open_location(item)
|
504
|
+
window.location_open
|
505
|
+
end
|
506
|
+
|
507
|
+
# Revert the current JSON document in the editor to the saved version.
|
508
|
+
def revert(item)
|
509
|
+
window.instance_eval do
|
510
|
+
@filename and file_open(@filename)
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
# Save the current JSON document.
|
515
|
+
def save(item)
|
516
|
+
window.file_save
|
517
|
+
end
|
518
|
+
|
519
|
+
# Save the current JSON document under the given filename.
|
520
|
+
def save_as(item)
|
521
|
+
window.file_save_as
|
522
|
+
end
|
523
|
+
|
524
|
+
# Quit the editor, after asking to save any unsaved changes first.
|
525
|
+
def quit(item)
|
526
|
+
window.quit
|
527
|
+
end
|
528
|
+
|
529
|
+
# Create the menu.
|
530
|
+
def create
|
531
|
+
title = MenuItem.new('File')
|
532
|
+
title.submenu = menu
|
533
|
+
add_item('New', &method(:new))
|
534
|
+
add_item('Open', ?o, &method(:open))
|
535
|
+
add_item('Open location', ?l, &method(:open_location))
|
536
|
+
add_item('Revert', &method(:revert))
|
537
|
+
add_separator
|
538
|
+
add_item('Save', ?s, &method(:save))
|
539
|
+
add_item('Save As', ?S, &method(:save_as))
|
540
|
+
add_separator
|
541
|
+
add_item('Quit', ?q, &method(:quit))
|
542
|
+
title
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
# This class creates the Edit pulldown menu.
|
547
|
+
class EditMenu
|
548
|
+
include MenuExtension
|
549
|
+
|
550
|
+
# Find a string in all nodes' contents and select the found node in the
|
551
|
+
# treeview.
|
552
|
+
def find(item)
|
553
|
+
search = ask_for_find_term or return
|
554
|
+
begin
|
555
|
+
@search = Regexp.new(search)
|
556
|
+
rescue => e
|
557
|
+
Editor.error_dialog(self, "Evaluation of regex /#{search}/ failed: #{e}!")
|
558
|
+
return
|
559
|
+
end
|
560
|
+
iter = model.get_iter('0')
|
561
|
+
iter.recursive_each do |i|
|
562
|
+
if @iter
|
563
|
+
if @iter != i
|
564
|
+
next
|
565
|
+
else
|
566
|
+
@iter = nil
|
567
|
+
next
|
568
|
+
end
|
569
|
+
elsif @search.match(i[CONTENT_COL])
|
570
|
+
set_cursor(i.path, nil, false)
|
571
|
+
@iter = i
|
572
|
+
break
|
573
|
+
end
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
# Repeat the last search given by #find.
|
578
|
+
def find_again(item)
|
579
|
+
@search or return
|
580
|
+
iter = model.get_iter('0')
|
581
|
+
iter.recursive_each do |i|
|
582
|
+
if @iter
|
583
|
+
if @iter != i
|
584
|
+
next
|
585
|
+
else
|
586
|
+
@iter = nil
|
587
|
+
next
|
588
|
+
end
|
589
|
+
elsif @search.match(i[CONTENT_COL])
|
590
|
+
set_cursor(i.path, nil, false)
|
591
|
+
@iter = i
|
592
|
+
break
|
593
|
+
end
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
# Sort (Reverse sort) all elements of the selected array by the given
|
598
|
+
# expression. _x_ is the element in question.
|
599
|
+
def sort(item)
|
600
|
+
if current = selection.selected
|
601
|
+
if current.type == 'Array'
|
602
|
+
parent = current.parent
|
603
|
+
ary = Editor.model2data(current)
|
604
|
+
order, reverse = ask_for_order
|
605
|
+
order or return
|
606
|
+
begin
|
607
|
+
block = eval "lambda { |x| #{order} }"
|
608
|
+
if reverse
|
609
|
+
ary.sort! { |a,b| block[b] <=> block[a] }
|
610
|
+
else
|
611
|
+
ary.sort! { |a,b| block[a] <=> block[b] }
|
612
|
+
end
|
613
|
+
rescue => e
|
614
|
+
Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
|
615
|
+
else
|
616
|
+
Editor.data2model(ary, model, parent) do |m|
|
617
|
+
m.insert_before(parent, current)
|
618
|
+
end
|
619
|
+
model.remove(current)
|
620
|
+
expand_collapse(parent)
|
621
|
+
window.change
|
622
|
+
toplevel.display_status("Array has been sorted.")
|
623
|
+
end
|
624
|
+
else
|
625
|
+
toplevel.display_status("Only Array nodes can be sorted!")
|
626
|
+
end
|
627
|
+
else
|
628
|
+
toplevel.display_status("Select an Array to sort first!")
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
# Create the menu.
|
633
|
+
def create
|
634
|
+
title = MenuItem.new('Edit')
|
635
|
+
title.submenu = menu
|
636
|
+
add_item('Find', ?f, &method(:find))
|
637
|
+
add_item('Find Again', ?g, &method(:find_again))
|
638
|
+
add_separator
|
639
|
+
add_item('Sort', ?S, &method(:sort))
|
640
|
+
title
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
class OptionsMenu
|
645
|
+
include MenuExtension
|
646
|
+
|
647
|
+
# Collapse/Expand all nodes by default.
|
648
|
+
def collapsed_nodes(item)
|
649
|
+
if expanded
|
650
|
+
self.expanded = false
|
651
|
+
collapse_all
|
652
|
+
else
|
653
|
+
self.expanded = true
|
654
|
+
expand_all
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
# Toggle pretty saving mode on/off.
|
659
|
+
def pretty_saving(item)
|
660
|
+
@pretty_item.toggled
|
661
|
+
window.change
|
662
|
+
end
|
663
|
+
|
664
|
+
attr_reader :pretty_item
|
665
|
+
|
666
|
+
# Create the menu.
|
667
|
+
def create
|
668
|
+
title = MenuItem.new('Options')
|
669
|
+
title.submenu = menu
|
670
|
+
add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes))
|
671
|
+
@pretty_item = add_item('Pretty saving', nil, CheckMenuItem,
|
672
|
+
&method(:pretty_saving))
|
673
|
+
@pretty_item.active = true
|
674
|
+
window.unchange
|
675
|
+
title
|
676
|
+
end
|
677
|
+
end
|
678
|
+
|
679
|
+
# This class inherits from Gtk::TreeView, to configure it and to add a lot
|
680
|
+
# of behaviour to it.
|
681
|
+
class JSONTreeView < Gtk::TreeView
|
682
|
+
include Gtk
|
683
|
+
|
684
|
+
# Creates a JSONTreeView instance, the parameter _window_ is
|
685
|
+
# a MainWindow instance and used for self delegation.
|
686
|
+
def initialize(window)
|
687
|
+
@window = window
|
688
|
+
super(TreeStore.new(Gdk::Pixbuf, String, String))
|
689
|
+
self.selection.mode = SELECTION_BROWSE
|
690
|
+
|
691
|
+
@expanded = false
|
692
|
+
self.headers_visible = false
|
693
|
+
add_columns
|
694
|
+
add_popup_menu
|
695
|
+
end
|
696
|
+
|
697
|
+
# Returns the MainWindow instance of this JSONTreeView.
|
698
|
+
attr_reader :window
|
699
|
+
|
700
|
+
# Returns true, if nodes are autoexpanding, false otherwise.
|
701
|
+
attr_accessor :expanded
|
702
|
+
|
703
|
+
private
|
704
|
+
|
705
|
+
def add_columns
|
706
|
+
cell = CellRendererPixbuf.new
|
707
|
+
column = TreeViewColumn.new('Icon', cell,
|
708
|
+
'pixbuf' => ICON_COL
|
709
|
+
)
|
710
|
+
append_column(column)
|
711
|
+
|
712
|
+
cell = CellRendererText.new
|
713
|
+
column = TreeViewColumn.new('Type', cell,
|
714
|
+
'text' => TYPE_COL
|
715
|
+
)
|
716
|
+
append_column(column)
|
717
|
+
|
718
|
+
cell = CellRendererText.new
|
719
|
+
cell.editable = true
|
720
|
+
column = TreeViewColumn.new('Content', cell,
|
721
|
+
'text' => CONTENT_COL
|
722
|
+
)
|
723
|
+
cell.signal_connect(:edited, &method(:cell_edited))
|
724
|
+
append_column(column)
|
725
|
+
end
|
726
|
+
|
727
|
+
def unify_key(iter, key)
|
728
|
+
return unless iter.type == 'Key'
|
729
|
+
parent = iter.parent
|
730
|
+
if parent.any? { |c| c != iter and c.content == key }
|
731
|
+
old_key = key
|
732
|
+
i = 0
|
733
|
+
begin
|
734
|
+
key = sprintf("%s.%d", old_key, i += 1)
|
735
|
+
end while parent.any? { |c| c != iter and c.content == key }
|
736
|
+
end
|
737
|
+
iter.content = key
|
738
|
+
end
|
739
|
+
|
740
|
+
def cell_edited(cell, path, value)
|
741
|
+
iter = model.get_iter(path)
|
742
|
+
case iter.type
|
743
|
+
when 'Key'
|
744
|
+
unify_key(iter, value)
|
745
|
+
toplevel.display_status('Key has been changed.')
|
746
|
+
when 'FalseClass'
|
747
|
+
value.downcase!
|
748
|
+
if value == 'true'
|
749
|
+
iter.type, iter.content = 'TrueClass', 'true'
|
750
|
+
end
|
751
|
+
when 'TrueClass'
|
752
|
+
value.downcase!
|
753
|
+
if value == 'false'
|
754
|
+
iter.type, iter.content = 'FalseClass', 'false'
|
755
|
+
end
|
756
|
+
when 'Numeric'
|
757
|
+
iter.content = (Integer(value) rescue Float(value) rescue 0).to_s
|
758
|
+
when 'String'
|
759
|
+
iter.content = value
|
760
|
+
when 'Hash', 'Array'
|
761
|
+
return
|
762
|
+
else
|
763
|
+
fail "Unknown type found in model: #{iter.type}"
|
764
|
+
end
|
765
|
+
window.change
|
766
|
+
end
|
767
|
+
|
768
|
+
def configure_value(value, type)
|
769
|
+
value.editable = false
|
770
|
+
case type
|
771
|
+
when 'Array', 'Hash'
|
772
|
+
value.text = ''
|
773
|
+
when 'TrueClass'
|
774
|
+
value.text = 'true'
|
775
|
+
when 'FalseClass'
|
776
|
+
value.text = 'false'
|
777
|
+
when 'NilClass'
|
778
|
+
value.text = 'null'
|
779
|
+
when 'Numeric', 'String'
|
780
|
+
value.text ||= ''
|
781
|
+
value.editable = true
|
782
|
+
else
|
783
|
+
raise ArgumentError, "unknown type '#{type}' encountered"
|
784
|
+
end
|
785
|
+
end
|
786
|
+
|
787
|
+
def add_popup_menu
|
788
|
+
menu = PopUpMenu.new(self)
|
789
|
+
menu.create
|
790
|
+
end
|
791
|
+
|
792
|
+
public
|
793
|
+
|
794
|
+
# Create a _type_ node with content _content_, and add it to _parent_
|
795
|
+
# in the model. If _parent_ is nil, create a new model and put it into
|
796
|
+
# the editor treeview.
|
797
|
+
def create_node(parent, type, content)
|
798
|
+
iter = if parent
|
799
|
+
model.append(parent)
|
800
|
+
else
|
801
|
+
new_model = Editor.data2model(nil)
|
802
|
+
toplevel.view_new_model(new_model)
|
803
|
+
new_model.iter_first
|
804
|
+
end
|
805
|
+
iter.type, iter.content = type, content
|
806
|
+
expand_collapse(parent) if parent
|
807
|
+
iter
|
808
|
+
end
|
809
|
+
|
810
|
+
# Ask for a hash key, value pair to be added to the Hash node _parent_.
|
811
|
+
def ask_for_hash_pair(parent)
|
812
|
+
key_input = type_input = value_input = nil
|
813
|
+
|
814
|
+
dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
|
815
|
+
[ Stock::OK, Dialog::RESPONSE_ACCEPT ],
|
816
|
+
[ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
|
817
|
+
)
|
818
|
+
|
819
|
+
hbox = HBox.new(false, 5)
|
820
|
+
hbox.pack_start(Label.new("Key:"))
|
821
|
+
hbox.pack_start(key_input = Entry.new)
|
822
|
+
key_input.text = @key || ''
|
823
|
+
dialog.vbox.add(hbox)
|
824
|
+
key_input.signal_connect(:activate) do
|
825
|
+
if parent.any? { |c| c.content == key_input.text }
|
826
|
+
toplevel.display_status('Key already exists in Hash!')
|
827
|
+
key_input.text = ''
|
828
|
+
else
|
829
|
+
toplevel.display_status('Key has been changed.')
|
830
|
+
end
|
831
|
+
end
|
832
|
+
|
833
|
+
hbox = HBox.new(false, 5)
|
834
|
+
hbox.add(Label.new("Type:"))
|
835
|
+
hbox.pack_start(type_input = ComboBox.new(true))
|
836
|
+
ALL_TYPES.each { |t| type_input.append_text(t) }
|
837
|
+
type_input.active = @type || 0
|
838
|
+
dialog.vbox.add(hbox)
|
839
|
+
|
840
|
+
type_input.signal_connect(:changed) do
|
841
|
+
value_input.editable = false
|
842
|
+
case ALL_TYPES[type_input.active]
|
843
|
+
when 'Array', 'Hash'
|
844
|
+
value_input.text = ''
|
845
|
+
when 'TrueClass'
|
846
|
+
value_input.text = 'true'
|
847
|
+
when 'FalseClass'
|
848
|
+
value_input.text = 'false'
|
849
|
+
when 'NilClass'
|
850
|
+
value_input.text = 'null'
|
851
|
+
else
|
852
|
+
value_input.text = ''
|
853
|
+
value_input.editable = true
|
854
|
+
end
|
855
|
+
end
|
856
|
+
|
857
|
+
hbox = HBox.new(false, 5)
|
858
|
+
hbox.add(Label.new("Value:"))
|
859
|
+
hbox.pack_start(value_input = Entry.new)
|
860
|
+
value_input.text = @value || ''
|
861
|
+
dialog.vbox.add(hbox)
|
862
|
+
|
863
|
+
dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
|
864
|
+
dialog.show_all
|
865
|
+
self.focus = dialog
|
866
|
+
dialog.run do |response|
|
867
|
+
if response == Dialog::RESPONSE_ACCEPT
|
868
|
+
@key = key_input.text
|
869
|
+
type = ALL_TYPES[@type = type_input.active]
|
870
|
+
content = value_input.text
|
871
|
+
return @key, type, content
|
872
|
+
end
|
873
|
+
end
|
874
|
+
return
|
875
|
+
ensure
|
876
|
+
dialog.destroy
|
877
|
+
end
|
878
|
+
|
879
|
+
# Ask for an element to be appended _parent_.
|
880
|
+
def ask_for_element(parent = nil, default_type = nil, value_text = @content)
|
881
|
+
type_input = value_input = nil
|
882
|
+
|
883
|
+
dialog = Dialog.new(
|
884
|
+
"New element into #{parent ? parent.type : 'root'}",
|
885
|
+
nil, nil,
|
886
|
+
[ Stock::OK, Dialog::RESPONSE_ACCEPT ],
|
887
|
+
[ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
|
888
|
+
)
|
889
|
+
hbox = HBox.new(false, 5)
|
890
|
+
hbox.add(Label.new("Type:"))
|
891
|
+
hbox.pack_start(type_input = ComboBox.new(true))
|
892
|
+
default_active = 0
|
893
|
+
types = parent ? ALL_TYPES : CONTAINER_TYPES
|
894
|
+
types.each_with_index do |t, i|
|
895
|
+
type_input.append_text(t)
|
896
|
+
if t == default_type
|
897
|
+
default_active = i
|
898
|
+
end
|
899
|
+
end
|
900
|
+
type_input.active = default_active
|
901
|
+
dialog.vbox.add(hbox)
|
902
|
+
type_input.signal_connect(:changed) do
|
903
|
+
configure_value(value_input, types[type_input.active])
|
904
|
+
end
|
905
|
+
|
906
|
+
hbox = HBox.new(false, 5)
|
907
|
+
hbox.add(Label.new("Value:"))
|
908
|
+
hbox.pack_start(value_input = Entry.new)
|
909
|
+
value_input.text = value_text if value_text
|
910
|
+
configure_value(value_input, types[type_input.active])
|
911
|
+
|
912
|
+
dialog.vbox.add(hbox)
|
913
|
+
|
914
|
+
dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
|
915
|
+
dialog.show_all
|
916
|
+
self.focus = dialog
|
917
|
+
dialog.run do |response|
|
918
|
+
if response == Dialog::RESPONSE_ACCEPT
|
919
|
+
type = types[type_input.active]
|
920
|
+
@content = case type
|
921
|
+
when 'Numeric'
|
922
|
+
Integer(value_input.text) rescue Float(value_input.text) rescue 0
|
923
|
+
else
|
924
|
+
value_input.text
|
925
|
+
end.to_s
|
926
|
+
return type, @content
|
927
|
+
end
|
928
|
+
end
|
929
|
+
return
|
930
|
+
ensure
|
931
|
+
dialog.destroy if dialog
|
932
|
+
end
|
933
|
+
|
934
|
+
# Ask for an order criteria for sorting, using _x_ for the element in
|
935
|
+
# question. Returns the order criterium, and true/false for reverse
|
936
|
+
# sorting.
|
937
|
+
def ask_for_order
|
938
|
+
dialog = Dialog.new(
|
939
|
+
"Give an order criterium for 'x'.",
|
940
|
+
nil, nil,
|
941
|
+
[ Stock::OK, Dialog::RESPONSE_ACCEPT ],
|
942
|
+
[ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
|
943
|
+
)
|
944
|
+
hbox = HBox.new(false, 5)
|
945
|
+
|
946
|
+
hbox.add(Label.new("Order:"))
|
947
|
+
hbox.pack_start(order_input = Entry.new)
|
948
|
+
order_input.text = @order || 'x'
|
949
|
+
|
950
|
+
hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'))
|
951
|
+
|
952
|
+
dialog.vbox.add(hbox)
|
953
|
+
|
954
|
+
dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
|
955
|
+
dialog.show_all
|
956
|
+
self.focus = dialog
|
957
|
+
dialog.run do |response|
|
958
|
+
if response == Dialog::RESPONSE_ACCEPT
|
959
|
+
return @order = order_input.text, reverse_checkbox.active?
|
960
|
+
end
|
961
|
+
end
|
962
|
+
return
|
963
|
+
ensure
|
964
|
+
dialog.destroy if dialog
|
965
|
+
end
|
966
|
+
|
967
|
+
# Ask for a find term to search for in the tree. Returns the term as a
|
968
|
+
# string.
|
969
|
+
def ask_for_find_term
|
970
|
+
dialog = Dialog.new(
|
971
|
+
"Find a node matching regex in tree.",
|
972
|
+
nil, nil,
|
973
|
+
[ Stock::OK, Dialog::RESPONSE_ACCEPT ],
|
974
|
+
[ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
|
975
|
+
)
|
976
|
+
hbox = HBox.new(false, 5)
|
977
|
+
|
978
|
+
hbox.add(Label.new("Regex:"))
|
979
|
+
hbox.pack_start(regex_input = Entry.new)
|
980
|
+
regex_input.text = @regex || ''
|
981
|
+
|
982
|
+
dialog.vbox.add(hbox)
|
983
|
+
|
984
|
+
dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
|
985
|
+
dialog.show_all
|
986
|
+
self.focus = dialog
|
987
|
+
dialog.run do |response|
|
988
|
+
if response == Dialog::RESPONSE_ACCEPT
|
989
|
+
return @regex = regex_input.text
|
990
|
+
end
|
991
|
+
end
|
992
|
+
return
|
993
|
+
ensure
|
994
|
+
dialog.destroy if dialog
|
995
|
+
end
|
996
|
+
|
997
|
+
# Expand or collapse row pointed to by _iter_ according
|
998
|
+
# to the #expanded attribute.
|
999
|
+
def expand_collapse(iter)
|
1000
|
+
if expanded
|
1001
|
+
expand_row(iter.path, true)
|
1002
|
+
else
|
1003
|
+
collapse_row(iter.path)
|
1004
|
+
end
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
# The editor main window
|
1009
|
+
class MainWindow < Gtk::Window
|
1010
|
+
include Gtk
|
1011
|
+
|
1012
|
+
def initialize(encoding)
|
1013
|
+
@changed = false
|
1014
|
+
@encoding = encoding
|
1015
|
+
super(TOPLEVEL)
|
1016
|
+
display_title
|
1017
|
+
set_default_size(800, 600)
|
1018
|
+
signal_connect(:delete_event) { quit }
|
1019
|
+
|
1020
|
+
vbox = VBox.new(false, 0)
|
1021
|
+
add(vbox)
|
1022
|
+
#vbox.border_width = 0
|
1023
|
+
|
1024
|
+
@treeview = JSONTreeView.new(self)
|
1025
|
+
@treeview.signal_connect(:'cursor-changed') do
|
1026
|
+
display_status('')
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
menu_bar = create_menu_bar
|
1030
|
+
vbox.pack_start(menu_bar, false, false, 0)
|
1031
|
+
|
1032
|
+
sw = ScrolledWindow.new(nil, nil)
|
1033
|
+
sw.shadow_type = SHADOW_ETCHED_IN
|
1034
|
+
sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
|
1035
|
+
vbox.pack_start(sw, true, true, 0)
|
1036
|
+
sw.add(@treeview)
|
1037
|
+
|
1038
|
+
@status_bar = Statusbar.new
|
1039
|
+
vbox.pack_start(@status_bar, false, false, 0)
|
1040
|
+
|
1041
|
+
@filename ||= nil
|
1042
|
+
if @filename
|
1043
|
+
data = read_data(@filename)
|
1044
|
+
view_new_model Editor.data2model(data)
|
1045
|
+
end
|
1046
|
+
end
|
1047
|
+
|
1048
|
+
# Creates the menu bar with the pulldown menus and returns it.
|
1049
|
+
def create_menu_bar
|
1050
|
+
menu_bar = MenuBar.new
|
1051
|
+
@file_menu = FileMenu.new(@treeview)
|
1052
|
+
menu_bar.append @file_menu.create
|
1053
|
+
@edit_menu = EditMenu.new(@treeview)
|
1054
|
+
menu_bar.append @edit_menu.create
|
1055
|
+
@options_menu = OptionsMenu.new(@treeview)
|
1056
|
+
menu_bar.append @options_menu.create
|
1057
|
+
menu_bar
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
# Sets editor status to changed, to indicate that the edited data
|
1061
|
+
# containts unsaved changes.
|
1062
|
+
def change
|
1063
|
+
@changed = true
|
1064
|
+
display_title
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
# Sets editor status to unchanged, to indicate that the edited data
|
1068
|
+
# doesn't containt unsaved changes.
|
1069
|
+
def unchange
|
1070
|
+
@changed = false
|
1071
|
+
display_title
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
# Puts a new model _model_ into the Gtk::TreeView to be edited.
|
1075
|
+
def view_new_model(model)
|
1076
|
+
@treeview.model = model
|
1077
|
+
@treeview.expanded = true
|
1078
|
+
@treeview.expand_all
|
1079
|
+
unchange
|
1080
|
+
end
|
1081
|
+
|
1082
|
+
# Displays _text_ in the status bar.
|
1083
|
+
def display_status(text)
|
1084
|
+
@cid ||= nil
|
1085
|
+
@status_bar.pop(@cid) if @cid
|
1086
|
+
@cid = @status_bar.get_context_id('dummy')
|
1087
|
+
@status_bar.push(@cid, text)
|
1088
|
+
end
|
1089
|
+
|
1090
|
+
# Opens a dialog, asking, if changes should be saved to a file.
|
1091
|
+
def ask_save
|
1092
|
+
if Editor.question_dialog(self,
|
1093
|
+
"Unsaved changes to JSON model. Save?")
|
1094
|
+
if @filename
|
1095
|
+
file_save
|
1096
|
+
else
|
1097
|
+
file_save_as
|
1098
|
+
end
|
1099
|
+
end
|
1100
|
+
end
|
1101
|
+
|
1102
|
+
# Quit this editor, that is, leave this editor's main loop.
|
1103
|
+
def quit
|
1104
|
+
ask_save if @changed
|
1105
|
+
destroy
|
1106
|
+
Gtk.main_quit
|
1107
|
+
true
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
# Display the new title according to the editor's current state.
|
1111
|
+
def display_title
|
1112
|
+
title = TITLE.dup
|
1113
|
+
title << ": #@filename" if @filename
|
1114
|
+
title << " *" if @changed
|
1115
|
+
self.title = title
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
# Clear the current model, after asking to save all unsaved changes.
|
1119
|
+
def clear
|
1120
|
+
ask_save if @changed
|
1121
|
+
@filename = nil
|
1122
|
+
self.view_new_model nil
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
def check_pretty_printed(json)
|
1126
|
+
pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
|
1127
|
+
@options_menu.pretty_item.active = pretty
|
1128
|
+
end
|
1129
|
+
private :check_pretty_printed
|
1130
|
+
|
1131
|
+
# Open the data at the location _uri_, if given. Otherwise open a dialog
|
1132
|
+
# to ask for the _uri_.
|
1133
|
+
def location_open(uri = nil)
|
1134
|
+
uri = ask_for_location unless uri
|
1135
|
+
uri or return
|
1136
|
+
data = load_location(uri) or return
|
1137
|
+
view_new_model Editor.data2model(data)
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
# Open the file _filename_ or call the #select_file method to ask for a
|
1141
|
+
# filename.
|
1142
|
+
def file_open(filename = nil)
|
1143
|
+
filename = select_file('Open as a JSON file') unless filename
|
1144
|
+
data = load_file(filename) or return
|
1145
|
+
view_new_model Editor.data2model(data)
|
1146
|
+
end
|
1147
|
+
|
1148
|
+
# Save the current file.
|
1149
|
+
def file_save
|
1150
|
+
if @filename
|
1151
|
+
store_file(@filename)
|
1152
|
+
else
|
1153
|
+
file_save_as
|
1154
|
+
end
|
1155
|
+
end
|
1156
|
+
|
1157
|
+
# Save the current file as the filename
|
1158
|
+
def file_save_as
|
1159
|
+
filename = select_file('Save as a JSON file')
|
1160
|
+
store_file(filename)
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
# Store the current JSON document to _path_.
|
1164
|
+
def store_file(path)
|
1165
|
+
if path
|
1166
|
+
data = Editor.model2data(@treeview.model.iter_first)
|
1167
|
+
File.open(path + '.tmp', 'wb') do |output|
|
1168
|
+
if @options_menu.pretty_item.active?
|
1169
|
+
output.puts JSON.pretty_generate(data)
|
1170
|
+
else
|
1171
|
+
output.write JSON.unparse(data)
|
1172
|
+
end
|
1173
|
+
end
|
1174
|
+
File.rename path + '.tmp', path
|
1175
|
+
@filename = path
|
1176
|
+
toplevel.display_status("Saved data to '#@filename'.")
|
1177
|
+
unchange
|
1178
|
+
end
|
1179
|
+
rescue SystemCallError => e
|
1180
|
+
Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
# Load the file named _filename_ into the editor as a JSON document.
|
1184
|
+
def load_file(filename)
|
1185
|
+
if filename
|
1186
|
+
if File.directory?(filename)
|
1187
|
+
Editor.error_dialog(self, "Try to select a JSON file!")
|
1188
|
+
return
|
1189
|
+
else
|
1190
|
+
data = read_data(filename)
|
1191
|
+
@filename = filename
|
1192
|
+
toplevel.display_status("Loaded data from '#@filename'.")
|
1193
|
+
display_title
|
1194
|
+
return data
|
1195
|
+
end
|
1196
|
+
end
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
# Load the data at location _uri_ into the editor as a JSON document.
|
1200
|
+
def load_location(uri)
|
1201
|
+
data = read_data(uri)
|
1202
|
+
@filename = nil
|
1203
|
+
toplevel.display_status("Loaded data from '#{uri}'.")
|
1204
|
+
display_title
|
1205
|
+
data
|
1206
|
+
end
|
1207
|
+
|
1208
|
+
# Read a JSON document from the file named _filename_, parse it into a
|
1209
|
+
# ruby data structure, and return the data.
|
1210
|
+
def read_data(filename)
|
1211
|
+
open(filename) do |f|
|
1212
|
+
json = f.read
|
1213
|
+
check_pretty_printed(json)
|
1214
|
+
if @encoding && !/^utf8$/i.match(@encoding)
|
1215
|
+
iconverter = Iconv.new('utf8', @encoding)
|
1216
|
+
json = iconverter.iconv(json)
|
1217
|
+
end
|
1218
|
+
return JSON::parse(json, :max_nesting => false)
|
1219
|
+
end
|
1220
|
+
rescue JSON::JSONError => e
|
1221
|
+
Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
|
1222
|
+
return
|
1223
|
+
rescue SystemCallError => e
|
1224
|
+
quit
|
1225
|
+
end
|
1226
|
+
|
1227
|
+
# Open a file selecton dialog, displaying _message_, and return the
|
1228
|
+
# selected filename or nil, if no file was selected.
|
1229
|
+
def select_file(message)
|
1230
|
+
filename = nil
|
1231
|
+
fs = FileSelection.new(message).set_modal(true).
|
1232
|
+
set_filename(Dir.pwd + "/").set_transient_for(self)
|
1233
|
+
fs.signal_connect(:destroy) { Gtk.main_quit }
|
1234
|
+
fs.ok_button.signal_connect(:clicked) do
|
1235
|
+
filename = fs.filename
|
1236
|
+
fs.destroy
|
1237
|
+
Gtk.main_quit
|
1238
|
+
end
|
1239
|
+
fs.cancel_button.signal_connect(:clicked) do
|
1240
|
+
fs.destroy
|
1241
|
+
Gtk.main_quit
|
1242
|
+
end
|
1243
|
+
fs.show_all
|
1244
|
+
Gtk.main
|
1245
|
+
filename
|
1246
|
+
end
|
1247
|
+
|
1248
|
+
# Ask for location URI a to load data from. Returns the URI as a string.
|
1249
|
+
def ask_for_location
|
1250
|
+
dialog = Dialog.new(
|
1251
|
+
"Load data from location...",
|
1252
|
+
nil, nil,
|
1253
|
+
[ Stock::OK, Dialog::RESPONSE_ACCEPT ],
|
1254
|
+
[ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
|
1255
|
+
)
|
1256
|
+
hbox = HBox.new(false, 5)
|
1257
|
+
|
1258
|
+
hbox.add(Label.new("Location:"))
|
1259
|
+
hbox.pack_start(location_input = Entry.new)
|
1260
|
+
location_input.width_chars = 60
|
1261
|
+
location_input.text = @location || ''
|
1262
|
+
|
1263
|
+
dialog.vbox.add(hbox)
|
1264
|
+
|
1265
|
+
dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
|
1266
|
+
dialog.show_all
|
1267
|
+
dialog.run do |response|
|
1268
|
+
if response == Dialog::RESPONSE_ACCEPT
|
1269
|
+
return @location = location_input.text
|
1270
|
+
end
|
1271
|
+
end
|
1272
|
+
return
|
1273
|
+
ensure
|
1274
|
+
dialog.destroy if dialog
|
1275
|
+
end
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
class << self
|
1279
|
+
# Starts a JSON Editor. If a block was given, it yields
|
1280
|
+
# to the JSON::Editor::MainWindow instance.
|
1281
|
+
def start(encoding = nil) # :yield: window
|
1282
|
+
encoding ||= 'utf8'
|
1283
|
+
Gtk.init
|
1284
|
+
@window = Editor::MainWindow.new(encoding)
|
1285
|
+
@window.icon_list = [ Editor.fetch_icon('json') ]
|
1286
|
+
yield @window if block_given?
|
1287
|
+
@window.show_all
|
1288
|
+
Gtk.main
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
attr_reader :window
|
1292
|
+
end
|
1293
|
+
end
|
1294
|
+
end
|
1295
|
+
# vim: set et sw=2 ts=2:
|