json.tcl 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. #
  2. # JSON parser for Tcl.
  3. #
  4. # See http://www.json.org/ && http://www.ietf.org/rfc/rfc4627.txt
  5. #
  6. # Copyright 2006 ActiveState Software Inc.
  7. #
  8. # $Id: json.tcl,v 1.2 2006/08/25 23:19:53 hobbs Exp $
  9. #
  10. if {$::tcl_version < 8.5} {
  11. package require dict
  12. }
  13. package provide json 1.0
  14. namespace eval json {}
  15. proc json::getc {{txtvar txt}} {
  16. # pop single char off the front of the text
  17. upvar 1 $txtvar txt
  18. if {$txt eq ""} {
  19. return -code error "unexpected end of text"
  20. }
  21. set c [string index $txt 0]
  22. set txt [string range $txt 1 end]
  23. return $c
  24. }
  25. proc json::json2dict {txt} {
  26. return [_json2dict]
  27. }
  28. proc json::_json2dict {{txtvar txt}} {
  29. upvar 1 $txtvar txt
  30. set state TOP
  31. set txt [string trimleft $txt]
  32. while {$txt ne ""} {
  33. set c [string index $txt 0]
  34. # skip whitespace
  35. while {[string is space $c]} {
  36. getc
  37. set c [string index $txt 0]
  38. }
  39. if {$c eq "\{"} {
  40. # object
  41. switch -- $state {
  42. TOP {
  43. # we are dealing with an Object
  44. getc
  45. set state OBJECT
  46. set dictVal [dict create]
  47. }
  48. VALUE {
  49. # this object element's value is an Object
  50. dict set dictVal $name [_json2dict]
  51. set state COMMA
  52. }
  53. LIST {
  54. # next element of list is an Object
  55. lappend listVal [_json2dict]
  56. set state COMMA
  57. }
  58. default {
  59. return -code error "unexpected open brace in $state mode"
  60. }
  61. }
  62. } elseif {$c eq "\}"} {
  63. getc
  64. if {$state ne "OBJECT" && $state ne "COMMA"} {
  65. return -code error "unexpected close brace in $state mode"
  66. }
  67. return $dictVal
  68. } elseif {$c eq ":"} {
  69. # name separator
  70. getc
  71. if {$state eq "COLON"} {
  72. set state VALUE
  73. } else {
  74. return -code error "unexpected colon in $state mode"
  75. }
  76. } elseif {$c eq ","} {
  77. # element separator
  78. if {$state eq "COMMA"} {
  79. getc
  80. if {[info exists listVal]} {
  81. set state LIST
  82. } elseif {[info exists dictVal]} {
  83. set state OBJECT
  84. }
  85. } else {
  86. return -code error "unexpected comma in $state mode"
  87. }
  88. } elseif {$c eq "\""} {
  89. # string
  90. # capture quoted string with backslash sequences
  91. set reStr {(?:(?:\")(?:[^\\\"]*(?:\\.[^\\\"]*)*)(?:\"))}
  92. set string ""
  93. if {![regexp $reStr $txt string]} {
  94. set txt [string replace $txt 32 end ...]
  95. return -code error "invalid formatted string in $txt"
  96. }
  97. set txt [string range $txt [string length $string] end]
  98. # chop off outer ""s and substitute backslashes
  99. # This does more than the RFC-specified backslash sequences,
  100. # but it does cover them all
  101. set string [subst -nocommand -novariable \
  102. [string range $string 1 end-1]]
  103. switch -- $state {
  104. TOP {
  105. return $string
  106. }
  107. OBJECT {
  108. set name $string
  109. set state COLON
  110. }
  111. LIST {
  112. lappend listVal $string
  113. set state COMMA
  114. }
  115. VALUE {
  116. dict set dictVal $name $string
  117. unset name
  118. set state COMMA
  119. }
  120. }
  121. } elseif {$c eq "\["} {
  122. # JSON array == Tcl list
  123. switch -- $state {
  124. TOP {
  125. getc
  126. set state LIST
  127. }
  128. LIST {
  129. lappend listVal [_json2dict]
  130. set state COMMA
  131. }
  132. VALUE {
  133. dict set dictVal $name [_json2dict]
  134. set state COMMA
  135. }
  136. default {
  137. return -code error "unexpected open bracket in $state mode"
  138. }
  139. }
  140. } elseif {$c eq "\]"} {
  141. # end of list
  142. getc
  143. if {![info exists listVal]} {
  144. #return -code error "unexpected close bracket in $state mode"
  145. # must be an empty list
  146. return ""
  147. }
  148. return $listVal
  149. } elseif {0 && $c eq "/"} {
  150. # comment
  151. # XXX: Not in RFC 4627
  152. getc
  153. set c [getc]
  154. switch -- $c {
  155. / {
  156. # // comment form
  157. set i [string first "\n" $txt]
  158. if {$i == -1} {
  159. set txt ""
  160. } else {
  161. set txt [string range $txt [incr i] end]
  162. }
  163. }
  164. * {
  165. # /* comment */ form
  166. getc
  167. set i [string first "*/" $txt]
  168. if {$i == -1} {
  169. return -code error "incomplete /* comment"
  170. } else {
  171. set txt [string range $txt [incr i] end]
  172. }
  173. }
  174. default {
  175. return -code error "unexpected slash in $state mode"
  176. }
  177. }
  178. } elseif {[string match {[-0-9]} $c]} {
  179. # one last check for a number, no leading zeros allowed,
  180. # but it may be 0.xxx
  181. string is double -failindex last $txt
  182. if {$last > 0} {
  183. set num [string range $txt 0 [expr {$last - 1}]]
  184. set txt [string range $txt $last end]
  185. switch -- $state {
  186. TOP {
  187. return $num
  188. }
  189. LIST {
  190. lappend listVal $num
  191. set state COMMA
  192. }
  193. VALUE {
  194. dict set dictVal $name $num
  195. set state COMMA
  196. }
  197. default {
  198. getc
  199. return -code error "unexpected number '$c' in $state mode"
  200. }
  201. }
  202. } else {
  203. getc
  204. return -code error "unexpected '$c' in $state mode"
  205. }
  206. } elseif {[string match {[ftn]} $c]
  207. && [regexp {^(true|false|null)} $txt val]} {
  208. # bare word value: true | false | null
  209. set txt [string range $txt [string length $val] end]
  210. switch -- $state {
  211. TOP {
  212. return $val
  213. }
  214. LIST {
  215. lappend listVal $val
  216. set state COMMA
  217. }
  218. VALUE {
  219. dict set dictVal $name $val
  220. set state COMMA
  221. }
  222. default {
  223. getc
  224. return -code error "unexpected '$c' in $state mode"
  225. }
  226. }
  227. } else {
  228. # error, incorrect format or unexpected end of text
  229. return -code error "unexpected '$c' in $state mode"
  230. }
  231. }
  232. }
  233. proc json::dict2json {dictVal} {
  234. # XXX: Currently this API isn't symmetrical, as to create proper
  235. # XXX: JSON text requires type knowledge of the input data
  236. set json ""
  237. dict for {key val} $dictVal {
  238. # key must always be a string, val may be a number, string or
  239. # bare word (true|false|null)
  240. if {0 && ![string is double -strict $val]
  241. && ![regexp {^(?:true|false|null)$} $val]} {
  242. set val "\"$val\""
  243. }
  244. append json "\"$key\": $val," \n
  245. }
  246. return "\{${json}\}"
  247. }
  248. proc json::list2json {listVal} {
  249. return "\[$[join $listVal ,]\]"
  250. }
  251. proc json::string2json {str} {
  252. return "\"$str\""
  253. }