Subject: 25.0.50; [PATCH] Add support for retrieving paths to JSON elements

From: Simen Heggestøyl <[email-masked]>
Date: Sat, 31 Oct 2015 09:46:02 +0100

Sometimes when working with a JSON structure, one needs to find the
path to a particular element. That can be tiresome to do manually,
especially if the structure is deep.

The proposed patch extends json.el with the ability to retrieve the
path to a particular JSON element.

See the following video for an example of how it can be used by an
interactive command, to show the path to the JSON element at point:

http://folk.uio.no/simenheg/json-mode.webm

The patch follows!

-- Simen

 From f6ddd3b797d6b0d92a1ffa0f5db59543ac7cdc11 Mon Sep 17 00:00:00 2001
 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= <[email-masked]>
Date: Sun, 25 Oct 2015 14:44:59 +0100
Subject: [PATCH] Add support for retrieving paths to JSON elements

Add support for retrieving the path to a JSON element. This can for
instance be useful to retrieve paths in deeply nested JSON
structures.

* lisp/json.el (json-path-to-position): New function for retrieving the
path to a JSON object at a given position.
(json--path-to-position, json--path): New variables, used internally by
`json-path-to-position'.
(json-read-object, json-read-array): Respect `json--path-to-position'.

* test/automated/json-tests.el (test-json-path-to-position-with-objects)
(test-json-path-to-position-with-arrays): New tests for
`json-path-to-position'.
---
 lisp/json.el | 67 ++++++++++++++++++++++++++++++++++++++++++--
 test/automated/json-tests.el | 14 +++++++++
 2 files changed, 79 insertions(+), 2 deletions(-)

diff --git a/lisp/json.el b/lisp/json.el
index b23d12a..2c82eee 100644
--- a/lisp/json.el
+++ b/lisp/json.el
@@ -196,6 +196,49 @@ 'json-end-of-file

+;;; Paths
+
+(defvar json--path-to-position nil
+ "When set to a position, `json-read' will return the path to
+the JSON element at the specified position, instead of returning
+the parsed JSON object.")
+
+(defvar json--path '()
+ "Used internally by `json-path-to-position' to keep track of
+the path during recursive calls to `json-read'.")
+
+(defun json-path-to-position (position &optional string)
+ "Return the path to the JSON element at POSITION.
+
+When STRING is provided, return the path to the position in the
+string, else to the position in the current buffer.
+
+The return value is a property list with the following
+properties:
+
+:path -- A list of strings and numbers forming the path to
+ the JSON element at the given position. Strings
+ denote object names, while numbers denote array
+ indexes.
+
+:match-start -- Position where the matched JSON element begins.
+
+:match-end -- Position where the matched JSON element ends.
+
+This can for instance be useful to determine the path to a JSON
+element in a deeply nested structure."
+ (save-excursion
+ (unless string
+ (goto-char (point-min)))
+ (let* ((json--path '())
+ (json--path-to-position position)
+ (path (catch :json-path
+ (if string
+ (json-read-from-string string)
+ (json-read)))))
+ (when (plist-get path :path)
+ path))))
+
 ;;; Keywords

 (defvar json-keywords '("true" "false" "null")
@@ -399,11 +442,21 @@ json-read-object
     (while (not (char-equal (json-peek) ?}))
       (json-skip-whitespace)
       (setq key (json-read-string))
+ (when json--path-to-position
+ (push key json--path))
       (json-skip-whitespace)
       (if (char-equal (json-peek) ?:)
           (json-advance)
         (signal 'json-object-format (list ":" (json-peek))))
- (setq value (json-read))
+ (json-skip-whitespace)
+ (let ((start (point)))
+ (setq value (json-read))
+ (when json--path-to-position
+ (when (< start json--path-to-position (+ (point) 1))
+ (throw :json-path (list :path (nreverse json--path)
+ :match-start start
+ :match-end (point))))
+ (pop json--path)))
       (setq elements (json-add-to-object elements key value))
       (json-skip-whitespace)
       (unless (char-equal (json-peek) ?})
@@ -509,7 +562,17 @@ json-read-array
   ;; read values until "]"
   (let (elements)
     (while (not (char-equal (json-peek) ?\]))
- (push (json-read) elements)
+ (json-skip-whitespace)
+ (when json--path-to-position
+ (push (length elements) json--path))
+ (let ((start (point)))
+ (push (json-read) elements)
+ (when json--path-to-position
+ (when (< start json--path-to-position (+ (point) 1))
+ (throw :json-path (list :path (nreverse json--path)
+ :match-start start
+ :match-end (point))))
+ (pop json--path)))
       (json-skip-whitespace)
       (unless (char-equal (json-peek) ?\])
         (if (char-equal (json-peek) ?,)
diff --git a/test/automated/json-tests.el b/test/automated/json-tests.el
index d1b7a2f..e0672dd 100644
--- a/test/automated/json-tests.el
+++ b/test/automated/json-tests.el
@@ -49,5 +49,19 @@
   (should (equal (json-read-from-string 
"\"\\nasd\\u0444\\u044b\\u0432fgh\\t\"")
                  "\nasdфывfgh\t")))

+(ert-deftest test-json-path-to-position-with-objects ()
+ (let* ((json-string "{\"foo\": {\"bar\": {\"baz\": \"value\"}}}")
+ (matched-path (json-path-to-position 32 json-string)))
+ (should (equal (plist-get matched-path :path) '("foo" "bar" "baz")))
+ (should (equal (plist-get matched-path :match-start) 25))
+ (should (equal (plist-get matched-path :match-end) 32))))
+
+(ert-deftest test-json-path-to-position-with-arrays ()
+ (let* ((json-string "{\"foo\": [\"bar\", [\"baz\"]]}")
+ (matched-path (json-path-to-position 20 json-string)))
+ (should (equal (plist-get matched-path :path) '("foo" 1 0)))
+ (should (equal (plist-get matched-path :match-start) 18))
+ (should (equal (plist-get matched-path :match-end) 23))))
+
 (provide 'json-tests)
 ;;; json-tests.el ends here
--
2.6.1