Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
John Griffiths
committed
Mar 17, 2009
1 parent
ea21b9a
commit 141095d
Showing
32 changed files
with
2,533 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
class String | ||
# Splits into sub-strings separated by ',' characters. Does not split | ||
# contents within {}, [], or "". \" does not end a string, \\" does. | ||
# Checks if closing characters match previous opening ones. | ||
def split_stateful | ||
memb = [] # list of members identified | ||
delims = [] # stack of delimiters | ||
split('').each { |c| | ||
memb << "" if memb.empty? | ||
case delims.last | ||
when '"' # quote mode | ||
c == '\\' and delims.push c | ||
c == '"' and delims.pop | ||
when '\\' # escape mode | ||
delims.pop | ||
else | ||
case c | ||
when '{', '[', '"' then delims.push c | ||
when ',' then ( memb << ""; c="" ) if delims.empty? # next element | ||
when '}' then delims.pop == '{' or raise RuntimeError, "Non-matching }." | ||
when ']' then delims.pop == '[' or raise RuntimeError, "Non-matching ]." | ||
end | ||
end | ||
memb[-1] += c | ||
} | ||
delims.empty? or raise RuntimeError, "No closing delimiter for #{delims.join(', ')}." | ||
memb | ||
end | ||
end | ||
|
||
class JSONParser | ||
|
||
NUM_FORMAT = /^(-)?(0|[1-9][0-9]*)(\.[0-9]+)?(E[+-]?([0-9]+))?$/i | ||
|
||
# parse_value | ||
def parse(code) | ||
code.strip! | ||
case code[0,1] | ||
when '"' then parse_string(code) | ||
when /[-0-9]/ then parse_number(code) | ||
when '{' then parse_object(code) | ||
when '[' then parse_array(code) | ||
else parse_keyword(code) | ||
end | ||
end | ||
|
||
def parse_string(code) | ||
code =~ /^"(.*)"$/ or raise RuntimeError, "String has no closing quotation mark." | ||
$_ = $1 | ||
$_ =~ /([^\\]|(\\\\)+)"/ and raise RuntimeError, "Non-escaped \" not allowed in string #{$_}." | ||
gsub(/\\(.)/) { |m| | ||
case $1 | ||
when 'b', 'f', 'n', 'r', 't' | ||
eval('"\\%s"' % $1) | ||
when 'u' | ||
m # no change, handled later | ||
when '"', '/', '\\' | ||
$1 # strip \ character | ||
else | ||
raise RuntimeError, "No such escape sequence \\#{$1}." | ||
end | ||
} | ||
gsub(/\\u([A-F0-9]{4})/i) { "%c" % $1.hex } | ||
end | ||
|
||
def parse_number(code) | ||
code =~ NUM_FORMAT or raise RuntimeError, "Invalid number #{code}." | ||
eval code | ||
end | ||
|
||
def parse_array(code) | ||
code =~ /^\[(.*)\]$/ or raise RuntimeError, "No closing bracket for array #{code}." | ||
$1.split_stateful.collect { |m| parse(m) } | ||
end | ||
|
||
def parse_object(code) | ||
code =~ /^\{(.*)\}$/ or raise RuntimeError, "No closing bracket for object #{code}." | ||
object = {} | ||
$1.split_stateful.each do |m| | ||
key, value = m.split(":", 2) | ||
object[parse_string(key.strip)] = parse(value) | ||
end | ||
object | ||
end | ||
|
||
def parse_keyword(code) | ||
case code | ||
when 'true', 'false' then eval(code) | ||
when 'null' then nil | ||
else | ||
raise RuntimeError, "Syntax error: #{code}." | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
require 'treetop' | ||
require 'json' # Note that we can require the Treetop file directly. | ||
require 'readline' | ||
|
||
parser = JsonParser.new | ||
while line = Readline::readline("? ", []) | ||
begin | ||
tree = parser.parse(line) | ||
if tree | ||
p tree.obj | ||
else | ||
puts parser.failure_reason | ||
end | ||
rescue => e | ||
puts e | ||
p e.backtrace | ||
p tree if tree | ||
end | ||
end | ||
puts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
# Treetop grammar for JSON for Ruby Quiz #155 by Clifford Heath. | ||
grammar Json | ||
rule json | ||
value | ||
end | ||
|
||
rule object | ||
'{' s pairs:pairs? s '}' s | ||
{ def obj | ||
pairs.empty? ? {} : pairs.obj | ||
end | ||
} | ||
end | ||
|
||
rule pairs | ||
member rest:(s ',' s member)* | ||
{ def obj | ||
rest.elements.inject({eval(member.k.text_value) => member.value.obj}) { |h, e| | ||
h[eval(e.member.k.text_value)] = e.member.value.obj | ||
h | ||
} | ||
end | ||
} | ||
end | ||
|
||
rule member # key/value pair of an object | ||
k:string s ':' s value | ||
end | ||
|
||
rule array | ||
'[' s e:elements? s ']' | ||
{ def obj | ||
e.empty? ? [] : e.obj | ||
end | ||
} | ||
end | ||
|
||
rule elements # elements of an array | ||
value rest:(s ',' s value)* | ||
{ def obj | ||
rest.elements.inject([value.obj]) { |a, e| | ||
a << e.value.obj | ||
} | ||
end | ||
} | ||
end | ||
|
||
rule value | ||
s alt:(string / number / object / array | ||
/ 'true' { def obj; true; end } | ||
/ 'false' { def obj; false; end } | ||
/ 'null' { def obj; nil; end } | ||
) | ||
{ def obj; alt.obj; end } | ||
end | ||
|
||
rule string | ||
'"' char* '"' { def obj | ||
eval( | ||
# Strip Unicode characters down to the chr equivalent. | ||
# Note that I'm cheating here: '"\\u4321"' should assert, | ||
# and there are cases that will succeed but corrupt the data. | ||
# This should be handled in the "char" rule. | ||
text_value.gsub(/\\u..../) { |unicode| | ||
eval("0x"+unicode[2..-1]).chr | ||
} | ||
) | ||
end | ||
} | ||
end | ||
|
||
rule char | ||
'\\' [\"\\\/bfnrt] | ||
/ '\\u' hex hex hex hex | ||
/ (![\\"] .) | ||
end | ||
|
||
rule hex | ||
[0-9A-Fa-f] | ||
end | ||
|
||
rule number | ||
int frac? exp? { def obj; eval(text_value); end } | ||
end | ||
|
||
rule int # Any integer | ||
'-'? ([1-9] [0-9]* / '0') | ||
{ def obj; eval(text_value); end } | ||
end | ||
|
||
rule frac # The fractional part of a floating-point number | ||
'.' [0-9]+ | ||
end | ||
|
||
rule exp # An exponent | ||
[eE] [-+]? [0-9]+ | ||
end | ||
|
||
rule s # Any amount of whtespace | ||
[ \t\n\t]* | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
class JSONParser | ||
def parse(text) | ||
parser = JsonParser.new | ||
p = parser.parse(text) | ||
raise parser.failure_reason unless p | ||
p.obj | ||
end | ||
end |
Oops, something went wrong.