1
2
3
4
5
6
7
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
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
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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
228
229
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
348
349
350
351
|
;;; json-snatcher.el --- Grabs the path to JSON values in a JSON file -*- lexical-binding: t -*-
;; Copyright (C) 2013 Sterling Graham <sterlingrgraham@gmail.com>
;; Author: Sterling Graham <sterlingrgraham@gmail.com>
;; URL: http://github.com/sterlingg/json-snatcher
;; Package-Version: 20150511.2047
;; Version: 1.0
;; Package-Requires: ((emacs "24"))
;; This file is not part of GNU Emacs.
;;; Commentary:
;;
;; Well this was my first excursion into ELisp programmming. It didn't go too badly once
;; I fiddled around with a bunch of the functions.
;;
;; The process of getting the path to a JSON value at point starts with
;; a call to the jsons-print-path function.
;;
;; It works by parsing the current buffer into a list of parse tree nodes
;; if the buffer hasn't already been parsed in the current Emacs session.
;; While parsing, the region occupied by the node is recorded into the
;; jsons-parsed-regions hash table as a list.The list contains the location
;; of the first character occupied by the node, the location of the last
;; character occupied, and the path to the node. The parse tree is also stored
;; in the jsons-parsed list for possible future use.
;;
;; Once the buffer has been parsed, the node at point is looked up in the
;; jsons-curr-region list, which is the list of regions described in the
;; previous paragraph for the current buffer. If point is not in one of these
;; interval ranges nil is returned, otherwise the path to the value is returned
;; in the form [<key-string>] for objects, and [<loc-int>] for arrays.
;; eg: ['value1'][0]['value2'] gets the array at with name value1, then gets the
;; 0th element of the array (another object), then gets the value at 'value2'.
;;
;;; Installation:
;;
;; IMPORTANT: Works ONLY in Emacs 24 due to the use of the lexical-binding variable.
;;
;; To install add the json-snatcher.el file to your load-path, and
;; add the following lines to your .emacs file:
;;(require 'json-snatcher)
;; (defun js-mode-bindings ()
;; "Sets a hotkey for using the json-snatcher plugin."
;; (when (string-match "\\.json$" (buffer-name))
;; (local-set-key (kbd "C-c C-g") 'jsons-print-path)))
;; (add-hook 'js-mode-hook 'js-mode-bindings)
;; (add-hook 'js2-mode-hook 'js-mode-bindings)
;;
;; This binds the key to snatch the path to the JSON value to C-c C-g only
;; when either JS mode, or JS2 mode is active on a buffer ending with
;; the .json extension.
;;; License:
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License
;; as published by the Free Software Foundation; either version 3
;; of the License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Code:
(defvar jsons-curr-token 0
"The current character in the buffer being parsed.")
(defvar jsons-parsed (make-hash-table :test 'equal)
"Hashes each open buffer to the parse tree for that buffer.")
(defvar jsons-parsed-regions (make-hash-table :test 'equal)
"Hashes each open buffer to the ranges in the buffer for each of the parse trees nodes.")
(defvar jsons-curr-region () "The node ranges in the current buffer.")
(defvar jsons-path-printer 'jsons-print-path-python "Default jsons path printer")
(add-hook 'kill-buffer-hook 'jsons-remove-buffer)
(defun jsons-consume-token ()
"Return the next token in the stream."
(goto-char jsons-curr-token)
(let* ((delim_regex "\\([\][\\{\\}:,]\\)")
;; TODO: Improve this regex. Although now it SEEMS to be working, and can be
;; used to validate escapes if needed later. The second half of the string regex is pretty
;; pointless at the moment. I did it this way, so that the code closely mirrors
;; the RFC.
(string_regex "\\(\"\\(\\([^\"\\\\\r\s\t\n]\\)*\\([\r\s\t\n]\\)*\\|\\(\\(\\\\\\\\\\)*\\\\\\(\\([^\r\s\t\n]\\|\\(u[0-9A-Fa-f]\\{4\\}\\)\\)\\)\\)\\)+\"\\)")
(num_regex "\\(-?\\(0\\|\\([1-9][[:digit:]]*\\)\\)\\(\\.[[:digit:]]+\\)?\\([eE][-+]?[[:digit:]]+\\)?\\)")
(literal_regex "\\(true\\|false\\|null\\)")
(full_regex (concat "\\(" delim_regex "\\|" literal_regex "\\|" string_regex "\\|" num_regex "\\)")))
(if (re-search-forward full_regex (point-max) "Not nil")
(progn
(setq jsons-curr-token (match-end 0))
(buffer-substring-no-properties (match-beginning 0) (match-end 0)))
(message "Reached EOF. Possibly invalid JSON."))))
(defun jsons-array (path)
"Create a new json array object that contain the identifier \"json-array\".
a list of the elements contained in the array, and the PATH to the array."
(let*(
(token (jsons-consume-token))
(array "json-array")
(elements ())
(i 0))
(while (not (string= token "]"))
(if (not (string= token ","))
(let ((json-val (jsons-value token path i)))
(setq i (+ i 1))
(push json-val elements)
(setq token (jsons-consume-token)))
(setq token (jsons-consume-token))))
(list array (reverse elements) path)))
(defun jsons-literal (token path)
"Given a TOKEN and PATH, this function return the PATH to the literal."
(let ((match_start (match-beginning 0))
(match_end (match-end 0)))
(progn
(setq jsons-curr-region (append (list (list match_start match_end path)) jsons-curr-region))
(list "json-literal" token path (list match_start match_end)))))
(defun jsons-member (token path)
"This function is called when a member in a JSON object needs to be parsed.
Given the current TOKEN, and the PATH to this member."
(let* ((member ())
(value token)
(range_start (match-beginning 0))
(range_end (match-end 0))
)
(setq member (list "json-member" token))
(if (not (string= (jsons-consume-token) ":"))
(error "Encountered token other than : in jsons-member")
nil)
(let ((json-val (jsons-value (jsons-consume-token) (cons value path) nil)))
(setq member (list member (append json-val
(list range_start range_end))))
(setq jsons-curr-region (append (list (list range_start range_end (elt json-val 2))) jsons-curr-region))
member)))
(defun jsons-number (token path)
"This function will return a json-number given by the current TOKEN.
PATH points to the path to this number. A json-number is defined as per
the num_regex in the `jsons-get-tokens' function."
(progn
(setq jsons-curr-region (append (list (list (match-beginning 0) (match-end 0) path)) jsons-curr-region))
(list "json-number" token path)))
(defun jsons-object (path)
"This function is called when a { is encountered while parsing.
PATH is the path in the tree to this object."
(let*(
(token (jsons-consume-token))
(members (make-hash-table :test 'equal))
(object (list "json-object" members path)))
(while (not (string= token "}"))
(if (not (string= token ","))
(let ((json-mem (jsons-member token path)))
(puthash (elt (elt json-mem 0) 1) (elt json-mem 1) (elt object 1))
(setq token (jsons-consume-token)))
(setq token (jsons-consume-token))))
object))
(defun jsons-string (token path)
"This function is called when a string is encountered while parsing.
The TOKEN is the current token being examined.
The PATH is the path to this string."
(let ((match_start (match-beginning 0))
(match_end (match-end 0)))
(progn
(setq jsons-curr-region (append (list (list match_start match_end path)) jsons-curr-region))
(list "json-string" token path (list match_start match_end)))))
(defun jsons-value (token path array-index)
"A value, which is either an object, array, string, number, or literal.
The is-array variable is nil if inside an array, or the index in
the array that it occupies.
TOKEN is the current token being parsed.
PATH is the path to this value.
ARRAY-INDEX is non-nil if the value is contained within an array, and
points to the index of this value in the containing array."
;;TODO: Refactor the if array-index statement.
(if array-index
(if (jsons-is-number token)
(list "json-value" (jsons-number token (cons array-index path)) (list (match-beginning 0) (match-end 0)))
(cond
((string= token "{") (jsons-object (cons array-index path)))
((string= token "[") (jsons-array (cons array-index path)))
((string= (substring token 0 1) "\"") (jsons-string token (cons array-index path)))
(t (jsons-literal token (cons array-index path)))))
(if (jsons-is-number token)
(list "json-value" (jsons-number token path) path (list (match-beginning 0) (match-end 0)))
(cond
((string= token "{") (jsons-object path))
((string= token "[") (jsons-array path))
((string= (substring token 0 1) "\"") (jsons-string token path))
(t (jsons-literal token path))))))
(defun jsons-get-path ()
"Function to check whether we can grab the json path from the cursor position in the json file."
(let ((i 0)
(node nil))
(setq jsons-curr-region (gethash (current-buffer) jsons-parsed-regions))
(when (not (gethash (current-buffer) jsons-parsed))
(jsons-parse))
(while (< i (length jsons-curr-region))
(let*
((json_region (elt jsons-curr-region i))
(min_token (elt json_region 0))
(max_token (elt json_region 1)))
(when (and (> (point) min_token) (< (point) max_token))
(setq node (elt json_region 2))))
(setq i (+ i 1)))
node))
(defun jsons-is-number (str)
"Test to see whether STR is a valid JSON number."
(progn
(match-end 0)
(save-match-data
(if (string-match "^\\(-?\\(0\\|\\([1-9][[:digit:]]*\\)\\)\\(\\.[[:digit:]]+\\)?\\([eE][-+]?[[:digit:]]+\\)?\\)$" str)
(progn
(match-end 0)
t)
nil))))
(defun jsons-parse ()
"Parse the file given in file, return a list of nodes representing the file."
(save-excursion
(setq jsons-curr-token 0)
(setq jsons-curr-region ())
(if (not (gethash (current-buffer) jsons-parsed))
(let* ((token (jsons-consume-token))
(return_val nil))
(cond
((string= token "{") (setq return_val (jsons-object ())))
((string= token "[") (setq return_val (jsons-array ())))
(t nil))
(puthash (current-buffer) return_val jsons-parsed)
(puthash (current-buffer) jsons-curr-region jsons-parsed-regions)
return_val)
(gethash (current-buffer) jsons-parsed))))
(defun jsons-print-to-buffer (node buffer)
"Prints the given NODE to the BUFFER specified in buffer argument.
TODO: Remove extra comma printed after lists of object members, and lists of array members."
(let ((id (elt node 0)))
(cond
((string= id "json-array")
(progn
(jsons-put-string buffer "[")
(mapc (lambda (x) (progn
(jsons-print-to-buffer buffer x)
(jsons-put-string buffer ",") )) (elt node 1))
(jsons-put-string buffer "]")))
((string= id "json-literal")
(jsons-put-string buffer (elt node 1)))
((string= id "json-member")
(jsons-put-string buffer (elt node 1))
(jsons-put-string buffer ": ")
(jsons-print-to-buffer buffer (elt node 2)))
((string= id "json-number")
(jsons-put-string buffer (elt node 1)))
((string= id "json-object")
(progn
(jsons-put-string buffer "{")
(maphash (lambda (key value)
(progn
(jsons-put-string buffer key)
(jsons-put-string buffer ":")
(jsons-print-to-buffer buffer value)
(jsons-put-string buffer ","))) (elt node 1))
(jsons-put-string buffer "}")))
((string= id "json-string")
(jsons-put-string buffer (elt node 1)))
((string= id "json-value")
(jsons-print-to-buffer buffer (elt node 1)))
(t nil))))
(defun jsons-print-path-jq ()
"Print the jq path to the JSON value under point, and save it in the kill ring."
(let* ((path (jsons-get-path))
(i 0)
(jq_str ".")
key)
(setq path (reverse path))
(while (< i (length path))
(if (numberp (elt path i))
(progn
(setq jq_str (concat jq_str "[" (number-to-string (elt path i)) "]"))
(setq i (+ i 1)))
(progn
(setq key (elt path i))
(setq jq_str (concat jq_str (substring key 1 (- (length key) 1))))
(setq i (+ i 1))))
(when (elt path i)
(unless (numberp (elt path i))
(setq jq_str (concat jq_str ".")))))
(progn (kill-new jq_str)
(princ jq_str))))
(defun jsons-print-path-python ()
"Print the python path to the JSON value under point, and save it in the kill ring."
(let ((path (jsons-get-path))
(i 0)
(python_str ""))
(setq path (reverse path))
(while (< i (length path))
(if (numberp (elt path i))
(progn
(setq python_str (concat python_str "[" (number-to-string (elt path i)) "]"))
(setq i (+ i 1)))
(progn
(setq python_str (concat python_str "[" (elt path i) "]"))
(setq i (+ i 1)))))
(progn (kill-new python_str)
(princ python_str))))
;;;###autoload
(defun jsons-print-path ()
"Print the path to the JSON value under point, and save it in the kill ring."
(interactive)
(funcall jsons-path-printer))
(defun jsons-put-string (buffer str)
"Append STR to the BUFFER specified in the argument."
(save-current-buffer
(set-buffer (get-buffer-create buffer))
(insert (prin1-to-string str t))))
(defun jsons-remove-buffer ()
"Used to clean up the token regions, and parse tree used by the parser."
(progn
(remhash (current-buffer) jsons-parsed)
(remhash (current-buffer) jsons-parsed-regions)))
(provide 'json-snatcher)
;; Local-Variables:
;; indent-tabs-mode: nil
;; End:
;;; json-snatcher.el ends here
|