Klimi's new dotfiles with stow.
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.

202 lines
7.1 KiB

4 years ago
  1. ;;; parseedn.el --- Clojure/EDN parser -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2017-2018 Arne Brasseur
  3. ;; Author: Arne Brasseur <arne@arnebrasseur.net>
  4. ;; Keywords: lisp clojure edn parser
  5. ;; Package-Version: 20190331.1058
  6. ;; Package-Requires: ((emacs "25") (a "0.1.0alpha4") (parseclj "0.1.0"))
  7. ;; Version: 0.1.0
  8. ;; This file is not part of GNU Emacs.
  9. ;; This file is free software; you can redistribute it and/or modify
  10. ;; it under the terms of the GNU General Public License as published by
  11. ;; the Free Software Foundation; either version 3, or (at your option)
  12. ;; any later version.
  13. ;; This file is distributed in the hope that it will be useful,
  14. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;; GNU General Public License for more details.
  17. ;; You should have received a copy of the GNU General Public License
  18. ;; along with GNU Emacs; see the file COPYING. If not, write to
  19. ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  20. ;; Boston, MA 02110-1301, USA.
  21. ;;; Commentary:
  22. ;; parseedn is an Emacs Lisp library for parsing EDN (Clojure) data.
  23. ;; It uses parseclj's shift-reduce parser internally.
  24. ;; EDN and Emacs Lisp have some important differences that make
  25. ;; translation from one to the other not transparent (think
  26. ;; representing an EDN map into Elisp, or being able to differentiate
  27. ;; between false and nil in Elisp). Because of this, parseedn takes
  28. ;; certain decisions when parsing and transforming EDN data into Elisp
  29. ;; data types. For more information please refer to parseclj's design
  30. ;; documentation.
  31. ;;; Code:
  32. ;; The EDN spec is not clear about whether \u0123 and \o012 are supported in
  33. ;; strings. They are described as character literals, but not as string escape
  34. ;; codes. In practice all implementations support them (mostly with broken
  35. ;; surrogate pair support), so we do the same. Sorry, emoji 🙁.
  36. ;;
  37. ;; Note that this is kind of broken, we don't correctly detect if \u or \o forms
  38. ;; don't have the right forms.
  39. (require 'a)
  40. (require 'cl-lib)
  41. (require 'parseclj-parser)
  42. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  43. ;; Reader
  44. (defvar parseedn-default-tag-readers
  45. (a-list 'inst (lambda (s)
  46. (cl-list* 'edn-inst (date-to-time s)))
  47. 'uuid (lambda (s)
  48. (list 'edn-uuid s)))
  49. "Default reader functions for handling tagged literals in EDN.
  50. These are the ones defined in the EDN spec, #inst and #uuid. It
  51. is not recommended you change this variable, as this globally
  52. changes the behavior of the EDN reader. Instead pass your own
  53. handlers as an optional argument to the reader functions.")
  54. (defun parseedn-reduce-leaf (stack token _options)
  55. "Put in the STACK an elisp value representing TOKEN.
  56. OPTIONS is an association list. See `parseclj-parse' for more information
  57. on available options."
  58. (if (member (parseclj-lex-token-type token) (list :whitespace :comment))
  59. stack
  60. (cons (parseclj-lex--leaf-token-value token) stack)))
  61. (defun parseedn-reduce-branch (stack opening-token children options)
  62. "Reduce STACK with an sequence containing a collection of other elisp values.
  63. Ignores discard tokens.
  64. OPENING-TOKEN is a lex token representing an opening paren, bracket or
  65. brace.
  66. CHILDREN is a collection elisp values to be reduced into an elisp
  67. sequence.
  68. OPTIONS is an association list. See `parseclj-parse' for more information
  69. on available options."
  70. (let ((tag-readers (a-merge parseedn-default-tag-readers (a-get options :tag-readers)))
  71. (token-type (parseclj-lex-token-type opening-token)))
  72. (if (eq token-type :discard)
  73. stack
  74. (cons
  75. (cl-case token-type
  76. (:root children)
  77. (:lparen children)
  78. (:lbracket (apply #'vector children))
  79. (:set (list 'edn-set children))
  80. (:lbrace (let* ((kvs (seq-partition children 2))
  81. (hash-map (make-hash-table :test 'equal :size (length kvs))))
  82. (seq-do (lambda (pair)
  83. (puthash (car pair) (cadr pair) hash-map))
  84. kvs)
  85. hash-map))
  86. (:tag (let* ((tag (intern (substring (a-get opening-token :form) 1)))
  87. (reader (a-get tag-readers tag :missing)))
  88. (when (eq :missing reader)
  89. (user-error "No reader for tag #%S in %S" tag (a-keys tag-readers)))
  90. (funcall reader (car children)))))
  91. stack))))
  92. (defun parseedn-read (&optional tag-readers)
  93. "Read content from current buffer and parse it as EDN source.
  94. Returns an Emacs Lisp value.
  95. TAG-READERS is an optional association list where keys are symbols
  96. identifying *tags*, and values are tag handler functions that receive one
  97. argument: *the tagged element*, and specify how to interpret it."
  98. (parseclj-parser #'parseedn-reduce-leaf
  99. #'parseedn-reduce-branch
  100. (a-list :tag-readers tag-readers)))
  101. (defun parseedn-read-str (s &optional tag-readers)
  102. "Parse string S as EDN.
  103. Returns an Emacs Lisp value.
  104. TAG-READERS is an optional association list. For more information, see
  105. `parseedn-read'."
  106. (with-temp-buffer
  107. (insert s)
  108. (goto-char 1)
  109. (car (parseedn-read tag-readers))))
  110. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  111. ;; Printer
  112. (defun parseedn-print-seq (coll)
  113. "Insert sequence COLL as EDN into the current buffer."
  114. (parseedn-print (elt coll 0))
  115. (let ((next (seq-drop coll 1)))
  116. (when (not (seq-empty-p next))
  117. (insert " ")
  118. (parseedn-print-seq next))))
  119. (defun parseedn-print-kvs (map)
  120. "Insert hash table MAP as an EDN map into the current buffer."
  121. (let ((keys (a-keys map)))
  122. (parseedn-print (car keys))
  123. (insert " ")
  124. (parseedn-print (a-get map (car keys)))
  125. (let ((next (cdr keys)))
  126. (when (not (seq-empty-p next))
  127. (insert ", ")
  128. (parseedn-print-kvs next)))))
  129. (defun parseedn-print (datum)
  130. "Insert DATUM as EDN into the current buffer.
  131. DATUM can be any Emacs Lisp value."
  132. (cond
  133. ((or (null datum) (numberp datum))
  134. (prin1 datum (current-buffer)))
  135. ((stringp datum)
  136. (insert "\"")
  137. (seq-doseq (char datum)
  138. (insert (cl-case char
  139. (?\t "\\t")
  140. (?\f "\\f")
  141. (?\" "\\\"")
  142. (?\r "\\r")
  143. (?\n"foo\t" "\\n")
  144. (?\\ "\\\\")
  145. (t (char-to-string char)))))
  146. (insert "\""))
  147. ((eq t datum)
  148. (insert "true"))
  149. ((symbolp datum)
  150. (insert (symbol-name datum)))
  151. ((vectorp datum) (insert "[") (parseedn-print-seq datum) (insert "]"))
  152. ((consp datum)
  153. (cond
  154. ((eq 'edn-set (car datum))
  155. (insert "#{") (parseedn-print-seq (cadr datum)) (insert "}"))
  156. (t (insert "(") (parseedn-print-seq datum) (insert ")"))))
  157. ((hash-table-p datum)
  158. (insert "{") (parseedn-print-kvs datum) (insert "}"))))
  159. (defun parseedn-print-str (datum)
  160. "Return a string containing DATUM as EDN.
  161. DATUM can be any Emacs Lisp value."
  162. (with-temp-buffer
  163. (parseedn-print datum)
  164. (buffer-substring-no-properties (point-min) (point-max))))
  165. (provide 'parseedn)
  166. ;;; parseedn.el ends here