Rebol [ Title: "JSON Parser for Rebol 3" Author: "Christopher Ross-Gill" Date: 18-Sep-2015 Home: http://www.ross-gill.com/page/JSON_and_Rebol File: %altjson.r Version: 0.3.6 Purpose: "Convert a Rebol block to a JSON string" Rights: http://opensource.org/licenses/Apache-2.0 Type: 'module Name: 'rgchris.altjson Exports: [load-json to-json] History: [ 18-Sep-2015 0.3.6 "Non-Word keys loaded as strings" 17-Sep-2015 0.3.5 "Added GET-PATH! lookup" 16-Sep-2015 0.3.4 "Reinstate /FLAT refinement" 21-Apr-2015 0.3.3 { - Merge from Reb4.me version - Recognise set-word pairs as objects - Use map! as the default object type - Serialize dates in RFC 3339 form } 14-Mar-2015 0.3.2 "Converts Json input to string before parsing" 07-Jul-2014 0.3.0 "Initial support for JSONP" 15-Jul-2011 0.2.6 "Flattens Flickr '_content' objects" 02-Dec-2010 0.2.5 "Support for time! added" 28-Aug-2010 0.2.4 "Encodes tag! any-type! paired blocks as an object" 06-Aug-2010 0.2.2 "Issue! composed of digits encoded as integers" 22-May-2005 0.1.0 "Original Version" ] Notes: { - Converts date! to RFC 3339 Date String } ] load-json: use [ tree branch here val is-flat emit new-child to-parent neaten to-word space comma number string block object _content value ident ][ branch: make block! 10 emit: func [val][here: insert/only here val] new-child: [(insert/only branch insert/only here here: copy [])] to-parent: [(here: take branch)] neaten: [ (new-line/all head here true) (new-line/all/skip head here true 2) ] to-word: use [word1 word+][ ; upper ranges borrowed from AltXML word1: charset [ "!&*.=?ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz|~" #"^(C0)" - #"^(D6)" #"^(D8)" - #"^(F6)" #"^(F8)" - #"^(02FF)" #"^(0370)" - #"^(037D)" #"^(037F)" - #"^(1FFF)" #"^(200C)" - #"^(200D)" #"^(2070)" - #"^(218F)" #"^(2C00)" - #"^(2FEF)" #"^(3001)" - #"^(D7FF)" #"^(f900)" - #"^(FDCF)" #"^(FDF0)" - #"^(FFFD)" ] word+: charset [ "!&'*+-.0123456789=?ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz|~" #"^(B7)" #"^(C0)" - #"^(D6)" #"^(D8)" - #"^(F6)" #"^(F8)" - #"^(037D)" #"^(037F)" - #"^(1FFF)" #"^(200C)" - #"^(200D)" #"^(203F)" - #"^(2040)" #"^(2070)" - #"^(218F)" #"^(2C00)" - #"^(2FEF)" #"^(3001)" - #"^(D7FF)" #"^(f900)" - #"^(FDCF)" #"^(FDF0)" - #"^(FFFD)" ] func [val [string!]][ all [ parse val [word1 any word+] to word! val ] ] ] space: use [space][ space: charset " ^-^/^M" [any space] ] comma: [space #"," space] number: use [dg ex nm as-num][ dg: charset "0123456789" ex: [[#"e" | #"E"] opt [#"+" | #"-"] some dg] nm: [opt #"-" some dg opt [#"." some dg] opt ex] as-num: func [val [string!]][ case [ not parse val [opt "-" some dg][to decimal! val] not integer? try [val: to integer! val][to issue! val] val [val] ] ] [copy val nm (val: as-num val)] ] string: use [ch es hx mp decode][ ch: complement charset {\"} es: charset {"\/bfnrt} hx: charset "0123456789ABCDEFabcdef" mp: [#"^"" "^"" #"\" "\" #"/" "/" #"b" "^H" #"f" "^L" #"r" "^M" #"n" "^/" #"t" "^-"] decode: use [ch mk escape][ escape: [ ; should be possible to use CHANGE keyword to replace escaped characters. mk: #"\" [ es (mk: change/part mk select mp mk/2 2) | #"u" copy ch 4 hx ( mk: change/part mk to char! to integer! debase/base ch 16 6 ) ] :mk ] func [text [string! none!]][ either none? text [make string! 0][ all [parse text [any [to "\" escape] to end] text] ] ] ] [#"^"" copy val [any [some ch | #"\" [#"u" 4 hx | es]]] #"^"" (val: decode val)] ] block: use [list][ list: [space opt [value any [comma value]] space] [#"[" new-child list #"]" neaten/1 to-parent] ] _content: [#"{" space {"_content"} space #":" space value space "}"] ; Flickr object: use [name list as-map][ name: [ string space #":" space ( emit either is-flat [ to tag! val ][ any [ to-word val val ] ] ) ] list: [space opt [name value any [comma name value]] space] as-map: [(unless is-flat [here: change back here make map! pick back here 1])] [#"{" new-child list #"}" neaten/2 to-parent as-map] ] ident: use [initial ident][ initial: charset ["$_" #"a" - #"z" #"A" - #"Z"] ident: union initial charset [#"0" - #"9"] [initial any ident] ] value: [ "null" (emit none) | "true" (emit true) | "false" (emit false) | number (emit val) | string (emit val) | _content | object | block ] func [ "Convert a JSON string to Rebol data" json [string! binary! file! url!] "JSON string" /flat "Objects are imported as tag-value pairs" /padded "Loads JSON data wrapped in a JSONP envelope" ][ case/all [ any [file? json url? json][ if error? json: try [read/string (json)][ do :json ] ] binary? json [json: to string! json] ] is-flat: :flat tree: here: copy [] either parse json either padded [ [space ident space "(" space opt value space ")" opt ";" space] ][ [space opt value space] ][ pick tree 1 ][ do make error! "Not a valid JSON string" ] ] ] to-json: use [ json emit emits escape emit-issue emit-date here lookup comma block object block-of-pairs value ][ emit: func [data][repend json data] emits: func [data][emit {"} emit data emit {"}] escape: use [mp ch encode][ mp: [#"^/" "\n" #"^M" "\r" #"^-" "\t" #"^"" "\^"" #"\" "\\" #"/" "\/"] ch: intersect ch: charset [#" " - #"~"] difference ch charset extract mp 2 encode: func [here][ change/part here any [ select mp here/1 join "\u" skip tail form to-hex to integer! here/1 -4 ] 1 ] func [txt][ parse txt [any [txt: some ch | skip (txt: encode txt) :txt]] head txt ] ] emit-issue: use [dg nm mk][ dg: charset "0123456789" nm: [opt "-" some dg] [(either parse next form here/1 [copy mk nm][emit mk][emits here/1])] ] emit-date: use [pad second][ pad: func [part length][part: to string! part head insert/dup part "0" length - length? part] [( emits rejoin collect [ keep reduce [pad here/1/year 4 "-" pad here/1/month 2 "-" pad here/1/day 2] if here/1/time [ keep reduce ["T" pad here/1/hour 2 ":" pad here/1/minute 2 ":"] keep either integer? here/1/second [ pad here/1/second 2 ][ second: split to string! here/1/second "." reduce [pad second/1 2 "." second/2] ] keep either any [ none? here/1/zone zero? here/1/zone ]["Z"][ reduce [ either here/1/zone/hour < 0 ["-"]["+"] pad abs here/1/zone/hour 2 ":" pad here/1/zone/minute 2 ] ] ] ] )] ] lookup: [ here: [get-word! | get-path!] (change here reduce reduce [here/1]) fail ] comma: [(if not tail? here [emit ","])] block: [ (emit "[") any [here: value here: comma] (emit "]") ] block-of-pairs: [ some [set-word! skip] | some [tag! skip] ] object: [ (emit "{") any [ here: [set-word! (change here to word! here/1) | any-string! | any-word!] (emit [{"} escape to string! here/1 {":}]) here: value here: comma ] (emit "}") ] value: [ lookup ; resolve a GET-WORD! reference | number! ( if percent? here/1 [change here to decimal! here/1] emit here/1 ) | [logic! | 'true | 'false] (emit to string! here/1) | [none! | 'none] (emit 'null) | date! emit-date | issue! emit-issue | [ any-string! | word! | lit-word! | tuple! | pair! | money! | time! ] (emits escape form here/1) | any-word! (emits escape form to word! here/1) | [object! | map!] :here (change/only here body-of first here) into object | into block-of-pairs :here (change/only here copy first here) into object | any-block! :here (change/only here copy first here) into block | any-type! (emits to tag! type? first here) ] func [ "Convert a Rebol value to JSON string" item [any-type!] "Rebol value to convert" ][ json: make string! "" if parse compose/only [(item)][here: value][json] ] ]