Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

352 lines
14 KiB

  1. ;;; json-snatcher.el --- Grabs the path to JSON values in a JSON file -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2013 Sterling Graham <sterlingrgraham@gmail.com>
  3. ;; Author: Sterling Graham <sterlingrgraham@gmail.com>
  4. ;; URL: http://github.com/sterlingg/json-snatcher
  5. ;; Package-Version: 1.0.0
  6. ;; Package-Commit: c4cecc0a5051bd364373aa499c47a1bb7a5ac51c
  7. ;; Version: 1.0
  8. ;; Package-Requires: ((emacs "24"))
  9. ;; This file is not part of GNU Emacs.
  10. ;;; Commentary:
  11. ;;
  12. ;; Well this was my first excursion into ELisp programmming. It didn't go too badly once
  13. ;; I fiddled around with a bunch of the functions.
  14. ;;
  15. ;; The process of getting the path to a JSON value at point starts with
  16. ;; a call to the jsons-print-path function.
  17. ;;
  18. ;; It works by parsing the current buffer into a list of parse tree nodes
  19. ;; if the buffer hasn't already been parsed in the current Emacs session.
  20. ;; While parsing, the region occupied by the node is recorded into the
  21. ;; jsons-parsed-regions hash table as a list.The list contains the location
  22. ;; of the first character occupied by the node, the location of the last
  23. ;; character occupied, and the path to the node. The parse tree is also stored
  24. ;; in the jsons-parsed list for possible future use.
  25. ;;
  26. ;; Once the buffer has been parsed, the node at point is looked up in the
  27. ;; jsons-curr-region list, which is the list of regions described in the
  28. ;; previous paragraph for the current buffer. If point is not in one of these
  29. ;; interval ranges nil is returned, otherwise the path to the value is returned
  30. ;; in the form [<key-string>] for objects, and [<loc-int>] for arrays.
  31. ;; eg: ['value1'][0]['value2'] gets the array at with name value1, then gets the
  32. ;; 0th element of the array (another object), then gets the value at 'value2'.
  33. ;;
  34. ;;; Installation:
  35. ;;
  36. ;; IMPORTANT: Works ONLY in Emacs 24 due to the use of the lexical-binding variable.
  37. ;;
  38. ;; To install add the json-snatcher.el file to your load-path, and
  39. ;; add the following lines to your .emacs file:
  40. ;;(require 'json-snatcher)
  41. ;; (defun js-mode-bindings ()
  42. ;; "Sets a hotkey for using the json-snatcher plugin."
  43. ;; (when (string-match "\\.json$" (buffer-name))
  44. ;; (local-set-key (kbd "C-c C-g") 'jsons-print-path)))
  45. ;; (add-hook 'js-mode-hook 'js-mode-bindings)
  46. ;; (add-hook 'js2-mode-hook 'js-mode-bindings)
  47. ;;
  48. ;; This binds the key to snatch the path to the JSON value to C-c C-g only
  49. ;; when either JS mode, or JS2 mode is active on a buffer ending with
  50. ;; the .json extension.
  51. ;;; License:
  52. ;; This program is free software; you can redistribute it and/or
  53. ;; modify it under the terms of the GNU General Public License
  54. ;; as published by the Free Software Foundation; either version 3
  55. ;; of the License, or (at your option) any later version.
  56. ;;
  57. ;; This program is distributed in the hope that it will be useful,
  58. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  59. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  60. ;; GNU General Public License for more details.
  61. ;;
  62. ;; You should have received a copy of the GNU General Public License
  63. ;; along with GNU Emacs; see the file COPYING. If not, write to the
  64. ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  65. ;; Boston, MA 02110-1301, USA.
  66. ;;; Code:
  67. (defvar jsons-curr-token 0
  68. "The current character in the buffer being parsed.")
  69. (defvar jsons-parsed (make-hash-table :test 'equal)
  70. "Hashes each open buffer to the parse tree for that buffer.")
  71. (defvar jsons-parsed-regions (make-hash-table :test 'equal)
  72. "Hashes each open buffer to the ranges in the buffer for each of the parse trees nodes.")
  73. (defvar jsons-curr-region () "The node ranges in the current buffer.")
  74. (defvar jsons-path-printer 'jsons-print-path-python "Default jsons path printer")
  75. (add-hook 'kill-buffer-hook 'jsons-remove-buffer)
  76. (defun jsons-consume-token ()
  77. "Return the next token in the stream."
  78. (goto-char jsons-curr-token)
  79. (let* ((delim_regex "\\([\][\\{\\}:,]\\)")
  80. ;; TODO: Improve this regex. Although now it SEEMS to be working, and can be
  81. ;; used to validate escapes if needed later. The second half of the string regex is pretty
  82. ;; pointless at the moment. I did it this way, so that the code closely mirrors
  83. ;; the RFC.
  84. (string_regex "\\(\"\\(\\([^\"\\\\\r\s\t\n]\\)*\\([\r\s\t\n]\\)*\\|\\(\\(\\\\\\\\\\)*\\\\\\(\\([^\r\s\t\n]\\|\\(u[0-9A-Fa-f]\\{4\\}\\)\\)\\)\\)\\)+\"\\)")
  85. (num_regex "\\(-?\\(0\\|\\([1-9][[:digit:]]*\\)\\)\\(\\.[[:digit:]]+\\)?\\([eE][-+]?[[:digit:]]+\\)?\\)")
  86. (literal_regex "\\(true\\|false\\|null\\)")
  87. (full_regex (concat "\\(" delim_regex "\\|" literal_regex "\\|" string_regex "\\|" num_regex "\\)")))
  88. (if (re-search-forward full_regex (point-max) "Not nil")
  89. (progn
  90. (setq jsons-curr-token (match-end 0))
  91. (buffer-substring-no-properties (match-beginning 0) (match-end 0)))
  92. (message "Reached EOF. Possibly invalid JSON."))))
  93. (defun jsons-array (path)
  94. "Create a new json array object that contain the identifier \"json-array\".
  95. a list of the elements contained in the array, and the PATH to the array."
  96. (let*(
  97. (token (jsons-consume-token))
  98. (array "json-array")
  99. (elements ())
  100. (i 0))
  101. (while (not (string= token "]"))
  102. (if (not (string= token ","))
  103. (let ((json-val (jsons-value token path i)))
  104. (setq i (+ i 1))
  105. (push json-val elements)
  106. (setq token (jsons-consume-token)))
  107. (setq token (jsons-consume-token))))
  108. (list array (reverse elements) path)))
  109. (defun jsons-literal (token path)
  110. "Given a TOKEN and PATH, this function return the PATH to the literal."
  111. (let ((match_start (match-beginning 0))
  112. (match_end (match-end 0)))
  113. (progn
  114. (setq jsons-curr-region (append (list (list match_start match_end path)) jsons-curr-region))
  115. (list "json-literal" token path (list match_start match_end)))))
  116. (defun jsons-member (token path)
  117. "This function is called when a member in a JSON object needs to be parsed.
  118. Given the current TOKEN, and the PATH to this member."
  119. (let* ((member ())
  120. (value token)
  121. (range_start (match-beginning 0))
  122. (range_end (match-end 0))
  123. )
  124. (setq member (list "json-member" token))
  125. (if (not (string= (jsons-consume-token) ":"))
  126. (error "Encountered token other than : in jsons-member")
  127. nil)
  128. (let ((json-val (jsons-value (jsons-consume-token) (cons value path) nil)))
  129. (setq member (list member (append json-val
  130. (list range_start range_end))))
  131. (setq jsons-curr-region (append (list (list range_start range_end (elt json-val 2))) jsons-curr-region))
  132. member)))
  133. (defun jsons-number (token path)
  134. "This function will return a json-number given by the current TOKEN.
  135. PATH points to the path to this number. A json-number is defined as per
  136. the num_regex in the `jsons-get-tokens' function."
  137. (progn
  138. (setq jsons-curr-region (append (list (list (match-beginning 0) (match-end 0) path)) jsons-curr-region))
  139. (list "json-number" token path)))
  140. (defun jsons-object (path)
  141. "This function is called when a { is encountered while parsing.
  142. PATH is the path in the tree to this object."
  143. (let*(
  144. (token (jsons-consume-token))
  145. (members (make-hash-table :test 'equal))
  146. (object (list "json-object" members path)))
  147. (while (not (string= token "}"))
  148. (if (not (string= token ","))
  149. (let ((json-mem (jsons-member token path)))
  150. (puthash (elt (elt json-mem 0) 1) (elt json-mem 1) (elt object 1))
  151. (setq token (jsons-consume-token)))
  152. (setq token (jsons-consume-token))))
  153. object))
  154. (defun jsons-string (token path)
  155. "This function is called when a string is encountered while parsing.
  156. The TOKEN is the current token being examined.
  157. The PATH is the path to this string."
  158. (let ((match_start (match-beginning 0))
  159. (match_end (match-end 0)))
  160. (progn
  161. (setq jsons-curr-region (append (list (list match_start match_end path)) jsons-curr-region))
  162. (list "json-string" token path (list match_start match_end)))))
  163. (defun jsons-value (token path array-index)
  164. "A value, which is either an object, array, string, number, or literal.
  165. The is-array variable is nil if inside an array, or the index in
  166. the array that it occupies.
  167. TOKEN is the current token being parsed.
  168. PATH is the path to this value.
  169. ARRAY-INDEX is non-nil if the value is contained within an array, and
  170. points to the index of this value in the containing array."
  171. ;;TODO: Refactor the if array-index statement.
  172. (if array-index
  173. (if (jsons-is-number token)
  174. (list "json-value" (jsons-number token (cons array-index path)) (list (match-beginning 0) (match-end 0)))
  175. (cond
  176. ((string= token "{") (jsons-object (cons array-index path)))
  177. ((string= token "[") (jsons-array (cons array-index path)))
  178. ((string= (substring token 0 1) "\"") (jsons-string token (cons array-index path)))
  179. (t (jsons-literal token (cons array-index path)))))
  180. (if (jsons-is-number token)
  181. (list "json-value" (jsons-number token path) path (list (match-beginning 0) (match-end 0)))
  182. (cond
  183. ((string= token "{") (jsons-object path))
  184. ((string= token "[") (jsons-array path))
  185. ((string= (substring token 0 1) "\"") (jsons-string token path))
  186. (t (jsons-literal token path))))))
  187. (defun jsons-get-path ()
  188. "Function to check whether we can grab the json path from the cursor position in the json file."
  189. (let ((i 0)
  190. (node nil))
  191. (setq jsons-curr-region (gethash (current-buffer) jsons-parsed-regions))
  192. (when (not (gethash (current-buffer) jsons-parsed))
  193. (jsons-parse))
  194. (while (< i (length jsons-curr-region))
  195. (let*
  196. ((json_region (elt jsons-curr-region i))
  197. (min_token (elt json_region 0))
  198. (max_token (elt json_region 1)))
  199. (when (and (> (point) min_token) (< (point) max_token))
  200. (setq node (elt json_region 2))))
  201. (setq i (+ i 1)))
  202. node))
  203. (defun jsons-is-number (str)
  204. "Test to see whether STR is a valid JSON number."
  205. (progn
  206. (match-end 0)
  207. (save-match-data
  208. (if (string-match "^\\(-?\\(0\\|\\([1-9][[:digit:]]*\\)\\)\\(\\.[[:digit:]]+\\)?\\([eE][-+]?[[:digit:]]+\\)?\\)$" str)
  209. (progn
  210. (match-end 0)
  211. t)
  212. nil))))
  213. (defun jsons-parse ()
  214. "Parse the file given in file, return a list of nodes representing the file."
  215. (save-excursion
  216. (setq jsons-curr-token 0)
  217. (setq jsons-curr-region ())
  218. (if (not (gethash (current-buffer) jsons-parsed))
  219. (let* ((token (jsons-consume-token))
  220. (return_val nil))
  221. (cond
  222. ((string= token "{") (setq return_val (jsons-object ())))
  223. ((string= token "[") (setq return_val (jsons-array ())))
  224. (t nil))
  225. (puthash (current-buffer) return_val jsons-parsed)
  226. (puthash (current-buffer) jsons-curr-region jsons-parsed-regions)
  227. return_val)
  228. (gethash (current-buffer) jsons-parsed))))
  229. (defun jsons-print-to-buffer (node buffer)
  230. "Prints the given NODE to the BUFFER specified in buffer argument.
  231. TODO: Remove extra comma printed after lists of object members, and lists of array members."
  232. (let ((id (elt node 0)))
  233. (cond
  234. ((string= id "json-array")
  235. (progn
  236. (jsons-put-string buffer "[")
  237. (mapc (lambda (x) (progn
  238. (jsons-print-to-buffer buffer x)
  239. (jsons-put-string buffer ",") )) (elt node 1))
  240. (jsons-put-string buffer "]")))
  241. ((string= id "json-literal")
  242. (jsons-put-string buffer (elt node 1)))
  243. ((string= id "json-member")
  244. (jsons-put-string buffer (elt node 1))
  245. (jsons-put-string buffer ": ")
  246. (jsons-print-to-buffer buffer (elt node 2)))
  247. ((string= id "json-number")
  248. (jsons-put-string buffer (elt node 1)))
  249. ((string= id "json-object")
  250. (progn
  251. (jsons-put-string buffer "{")
  252. (maphash (lambda (key value)
  253. (progn
  254. (jsons-put-string buffer key)
  255. (jsons-put-string buffer ":")
  256. (jsons-print-to-buffer buffer value)
  257. (jsons-put-string buffer ","))) (elt node 1))
  258. (jsons-put-string buffer "}")))
  259. ((string= id "json-string")
  260. (jsons-put-string buffer (elt node 1)))
  261. ((string= id "json-value")
  262. (jsons-print-to-buffer buffer (elt node 1)))
  263. (t nil))))
  264. (defun jsons-print-path-jq ()
  265. "Print the jq path to the JSON value under point, and save it in the kill ring."
  266. (let* ((path (jsons-get-path))
  267. (i 0)
  268. (jq_str ".")
  269. key)
  270. (setq path (reverse path))
  271. (while (< i (length path))
  272. (if (numberp (elt path i))
  273. (progn
  274. (setq jq_str (concat jq_str "[" (number-to-string (elt path i)) "]"))
  275. (setq i (+ i 1)))
  276. (progn
  277. (setq key (elt path i))
  278. (setq jq_str (concat jq_str (substring key 1 (- (length key) 1))))
  279. (setq i (+ i 1))))
  280. (when (elt path i)
  281. (unless (numberp (elt path i))
  282. (setq jq_str (concat jq_str ".")))))
  283. (progn (kill-new jq_str)
  284. (princ jq_str))))
  285. (defun jsons-print-path-python ()
  286. "Print the python path to the JSON value under point, and save it in the kill ring."
  287. (let ((path (jsons-get-path))
  288. (i 0)
  289. (python_str ""))
  290. (setq path (reverse path))
  291. (while (< i (length path))
  292. (if (numberp (elt path i))
  293. (progn
  294. (setq python_str (concat python_str "[" (number-to-string (elt path i)) "]"))
  295. (setq i (+ i 1)))
  296. (progn
  297. (setq python_str (concat python_str "[" (elt path i) "]"))
  298. (setq i (+ i 1)))))
  299. (progn (kill-new python_str)
  300. (princ python_str))))
  301. ;;;###autoload
  302. (defun jsons-print-path ()
  303. "Print the path to the JSON value under point, and save it in the kill ring."
  304. (interactive)
  305. (funcall jsons-path-printer))
  306. (defun jsons-put-string (buffer str)
  307. "Append STR to the BUFFER specified in the argument."
  308. (save-current-buffer
  309. (set-buffer (get-buffer-create buffer))
  310. (insert (prin1-to-string str t))))
  311. (defun jsons-remove-buffer ()
  312. "Used to clean up the token regions, and parse tree used by the parser."
  313. (progn
  314. (remhash (current-buffer) jsons-parsed)
  315. (remhash (current-buffer) jsons-parsed-regions)))
  316. (provide 'json-snatcher)
  317. ;; Local-Variables:
  318. ;; indent-tabs-mode: nil
  319. ;; End:
  320. ;;; json-snatcher.el ends here