131 lines
2.7 KiB
CoffeeScript
131 lines
2.7 KiB
CoffeeScript
# This code works with Lisp-like s-expressions.
|
|
#
|
|
# We lex tokens then do recursive descent on the tokens
|
|
# to build our data structures.
|
|
|
|
sexp = (data) ->
|
|
# Convert a JS data structure to a string s-expression. A sexier version
|
|
# would remove quotes around strings that don't need them.
|
|
s = ''
|
|
if Array.isArray data
|
|
children = (sexp elem for elem in data).join ' '
|
|
'(' + children + ')'
|
|
else
|
|
return JSON.stringify data
|
|
|
|
parse_sexp = (sexp) ->
|
|
tokens = lex_sexp sexp
|
|
i = 0
|
|
|
|
_parse_list = ->
|
|
i += 1
|
|
arr = []
|
|
while i < tokens.length and tokens[i].type != ')'
|
|
arr.push _parse()
|
|
if i < tokens.length
|
|
i += 1
|
|
else
|
|
throw Error "missing end paren"
|
|
arr
|
|
|
|
_guess_type = (word) ->
|
|
# This is crude, doesn't handle all forms of floats.
|
|
if word.match /^\d+\.\d+$/
|
|
parseFloat(word)
|
|
else if word.match /^\d+/
|
|
parseInt(word)
|
|
else
|
|
word
|
|
|
|
_parse_word = ->
|
|
token = tokens[i]
|
|
i += 1
|
|
if token.type == 'string'
|
|
token.word
|
|
else
|
|
_guess_type token.word
|
|
|
|
_parse = ->
|
|
return undefined unless i < tokens.length
|
|
token = tokens[i]
|
|
if token.type == '('
|
|
_parse_list()
|
|
else
|
|
_parse_word()
|
|
|
|
exp = _parse()
|
|
throw Error "premature termination" if i < tokens.length
|
|
exp
|
|
|
|
lex_sexp = (sexp) ->
|
|
is_whitespace = (c) -> c in [' ', '\t', '\n']
|
|
i = 0
|
|
tokens = []
|
|
|
|
test = (f) ->
|
|
return false unless i < sexp.length
|
|
f(sexp[i])
|
|
|
|
eat_char = (c) ->
|
|
tokens.push
|
|
type: c
|
|
i += 1
|
|
|
|
eat_whitespace = ->
|
|
i += 1
|
|
while test is_whitespace
|
|
i += 1
|
|
|
|
eat_word = ->
|
|
token = c
|
|
i += 1
|
|
word_char = (c) ->
|
|
c != ')' and !is_whitespace c
|
|
while test word_char
|
|
token += sexp[i]
|
|
i += 1
|
|
tokens.push
|
|
type: "word"
|
|
word: token
|
|
|
|
eat_quoted_word = ->
|
|
start = i
|
|
i += 1
|
|
token = ''
|
|
while test ((c) -> c != '"')
|
|
if sexp[i] == '\\'
|
|
i += 1
|
|
throw Error("escaping error") unless i < sexp.length
|
|
token += sexp[i]
|
|
i += 1
|
|
if test ((c) -> c == '"')
|
|
tokens.push
|
|
type: "string"
|
|
word: token
|
|
i += 1
|
|
else
|
|
throw Error("end quote missing #{sexp.substring(start, i)}")
|
|
|
|
while i < sexp.length
|
|
c = sexp[i]
|
|
if c == '(' or c == ')'
|
|
eat_char c
|
|
else if is_whitespace c
|
|
eat_whitespace()
|
|
else if c == '"'
|
|
eat_quoted_word()
|
|
else
|
|
eat_word()
|
|
tokens
|
|
|
|
do ->
|
|
input = """
|
|
((data "quoted data with escaped \\"" 123 4.5 "14")
|
|
(data (!@# (4.5) "(more" "data)")))
|
|
"""
|
|
console.log "input:\n#{input}\n"
|
|
output = parse_sexp(input)
|
|
pp = (data) -> JSON.stringify data, null, ' '
|
|
console.log "output:\n#{pp output}\n"
|
|
console.log "round trip:\n#{sexp output}\n"
|