Tcl Library Source Code

Artifact [b6b3a1889b]
Login
EuroTcl/OpenACS 11 - 12 JULY 2024, VIENNA

Artifact b6b3a1889bc60b5993ccde0182a4486bba612087:

Attachment "json.tcl" to ticket [2967134fff] added by cwjolly 2010-03-10 10:04:49.
#
#   JSON parser for Tcl.
#
#   See http://www.json.org/ && http://www.ietf.org/rfc/rfc4627.txt
#
#   Copyright 2006 ActiveState Software Inc.
#
#   $Id: json.tcl,v 1.2 2006/08/25 23:19:53 hobbs Exp $
#

if {$::tcl_version < 8.5} {
    package require dict
}

package provide json 1.1

namespace eval json {
     variable autoindex false
}

proc json::getc {{txtvar txt}} {
    # pop single char off the front of the text
    upvar 1 $txtvar txt
    if {$txt eq ""} {
    	return -code error "unexpected end of text"
    }

    set c [string index $txt 0]
    set txt [string range $txt 1 end]
    return $c
}

proc json::json2dict {txt args } {
    variable autoindex 
    array set opts { -indexlists 0 } 
    array set opts $args
    if { [array size opts ] > 1 } {
	foreach item [array names opts ] {
	    if { ![string match "-indexlists" $item ] } {
		lappend extraopt $item
	    }
	}
	return -code error "unexpected option(s) [join $extraopt , ] ! Only -indexlists option permitted"
    }
    set autoindex $opts(-indexlists) 
    return [_json2dict]
}

proc json::_json2dict {{txtvar txt}} {
    variable autoindex 
    upvar 1 $txtvar txt
    set lcount -1 
    set state TOP

    set txt [string trimleft $txt]
    while {$txt ne ""} {
    	set c [string index $txt 0]

    	# skip whitespace
    	while {[string is space $c]} {
    	    getc
    	    set c [string index $txt 0]
    	}

	if {$c eq "\{"} {
	    # object
	    switch -- $state {
		TOP {
		    # we are dealing with an Object
		    getc
		    set state OBJECT
		    set dictVal [dict create]
		}
		VALUE {
		    # this object element's value is an Object
		    dict set dictVal $name [_json2dict]
		    set state COMMA
		}
		LIST {
		    # next element of list is an Object
		    if { $autoindex } {
			lappend listVal [incr lcount] [_json2dict]
		    } else {
			lappend listVal  [_json2dict]
		    }			
		    set state COMMA
		}
		default {
		    return -code error "unexpected open brace in $state mode"
		}
	    }
	} elseif {$c eq "\}"} {
	    getc
	    if {$state ne "OBJECT" && $state ne "COMMA"} {
		return -code error "unexpected close brace in $state mode"
	    }
	    return $dictVal
	} elseif {$c eq ":"} {
	    # name separator
	    getc

	    if {$state eq "COLON"} {
		set state VALUE
	    } else {
		return -code error "unexpected colon in $state mode"
	    } 
	} elseif {$c eq ","} {
	    # element separator
	    if {$state eq "COMMA"} {
		getc
		if {[info exists listVal]} {
		    set state LIST
		} elseif {[info exists dictVal]} {
		    set state OBJECT
		}
	    } else {
		return -code error "unexpected comma in $state mode"
	    }
	} elseif {$c eq "\""} {
	    # string
	    # capture quoted string with backslash sequences
	    set reStr {(?:(?:\")(?:[^\\\"]*(?:\\.[^\\\"]*)*)(?:\"))}
	    set string ""
	    if {![regexp $reStr $txt string]} {
		set txt [string replace $txt 32 end ...]
		return -code error "invalid formatted string in $txt"
	    }
	    set txt [string range $txt [string length $string] end]
	    # chop off outer ""s and substitute backslashes
	    # This does more than the RFC-specified backslash sequences,
	    # but it does cover them all
	    set string [subst -nocommand -novariable \
			    [string range $string 1 end-1]]

	    switch -- $state {
		TOP {
		    return $string
		}
		OBJECT {
		    set name $string
		    set state COLON
		}
		LIST {
		    if { $autoindex } {
			lappend listVal [incr lcount] [_json2dict]
		    } else {
			lappend listVal [_json2dict]
		    }			
		    set state COMMA
		}
		VALUE {
		    dict set dictVal $name \"$string\"
		    unset name
		    set state COMMA
		}
	    }
	} elseif {$c eq "\["} {
	    # JSON array == Tcl list
	    switch -- $state {
		TOP {
		    getc
		    set state LIST
		    set lcount -1
		}
		LIST {
		    if { $autoindex } {
			lappend listVal [incr lcount] [_json2dict]
		    } else {
			lappend listVal [_json2dict]
		    }			
		    set state COMMA
		}
		VALUE {
		    dict set dictVal $name [_json2dict]
		    set state COMMA
		}
		default {
		    return -code error "unexpected open bracket in $state mode"
		}
	    }
	} elseif {$c eq "\]"} {
	    # end of list
	    getc
	    if {![info exists listVal]} {
		#return -code error "unexpected close bracket in $state mode"
		# must be an empty list
		return ""
	    }
	    return $listVal
	} elseif {0 && $c eq "/"} {
	    # comment
	    # XXX: Not in RFC 4627
	    getc
	    set c [getc]
	    switch -- $c {
		/ {
		    # // comment form
		    set i [string first "\n" $txt]
		    if {$i == -1} {
			set txt ""
		    } else {
			set txt [string range $txt [incr i] end]
		    }
		}
		* {
		    # /* comment */ form
		    getc
		    set i [string first "*/" $txt]
		    if {$i == -1} {
			return -code error "incomplete /* comment"
		    } else {
			set txt [string range $txt [incr i] end]
		    }
		}
		default {
		    return -code error "unexpected slash in $state mode"
		}
	    }
	} elseif {[string match {[-0-9]} $c]} {
	    # one last check for a number, no leading zeros allowed,
	    # but it may be 0.xxx
	    string is double -failindex last $txt
	    if {$last > 0} {
		set num [string range $txt 0 [expr {$last - 1}]]
		set txt [string range $txt $last end]

		switch -- $state {
		    TOP {
			return $num
		    }
		    LIST {
			if { $autoindex } {
			    lappend listVal [incr lcount] [_json2dict]
			} else {
			    lappend listVal [_json2dict]
			}			
			set state COMMA
		    }
		    VALUE {
			dict set dictVal $name $num
			set state COMMA
		    }
		    default {
			getc
			return -code error "unexpected number '$c' in $state mode"
		    }
		}
	    } else {
		getc
		return -code error "unexpected '$c' in $state mode"
	    }
	} elseif {[string match {[ftn]} $c]
		  && [regexp {^(true|false|null)} $txt val]} {
	    # bare word value: true | false | null
	    set txt [string range $txt [string length $val] end]

	    switch -- $state {
		TOP {
		    return $val
		}
		LIST {
		    lappend listVal [incr lcount ] $val
		    set state COMMA
		}
		VALUE {
		    dict set dictVal $name $val
		    set state COMMA
		}
		default {
		    getc
		    return -code error "unexpected '$c' in $state mode"
		}
	    }
	} else {
	    # error, incorrect format or unexpected end of text
	    return -code error "unexpected '$c' in $state mode"
	}
    }
}

proc json::dict2json {dictVal} {
    # XXX: Currently this API isn't symmetrical, as to create proper
    # XXX: JSON text requires type knowledge of the input data
    set json ""

    dict for {key val} $dictVal {
	# key must always be a string, val may be a number, string or
	# bare word (true|false|null)
	if {0 && ![string is double -strict $val]
	    && ![regexp {^(?:true|false|null)$} $val]} {
	    set val "\"$val\""
	}
    	append json "\"$key\": $val," \n
    }

    return "\{${json}\}"
}

proc json::list2json {listVal} {
    return "\[$[join $listVal ,]\]"
}

# note this is for numerical indexes only arrays must have indexes 0 ... onward
proc json::array2json {inarray} {
    upvar $inarray arrayVal
    foreach key [lsort -integer [array names arrayVal] {
	lappend temp $arrayVal($key)
    }
    return list2json $temp;
}

proc json::string2json {str} {
    return "\"$str\""
}