A Template literal converts s-expression to json which support variable embedding.
you can play tsjson and sxml in browser in [playground.html][playground].
[playground]: https://gholk.gitlab.io/tsjson/playground.html
js
import tsj from '@gholk/tsjson' // or 'path/to/tsjson/index.esm.js'
const j = tsj.jconst jo = j
1 2 a b ("str1" "str2") (:ok false :val null)
// [1, 2, 'a', 'b', ['str1', 'str2'], {ok: false, val: null}]const jv = j
type-${jo[2]} ${x => x*2} "1 + 2 = ${1+2}\\n"
// ['type-a', x=>x*2, "1 + 2 = 3\n"]
`with plain string:
`js
j('1 2 (x 0) ${1+2}\nl2 \t "escape sequence \\n \\\\ \\t end"')
// [1, 2, ['x', 0], '${1+2}', 'l2', 'escape sequence \n \\ \t end']
`array
The whole s-expression is wrap in brackets automatically.`js
j 1 // [1]
j 1 2 3 // [1, 2, 3]
j // []
j 1 (2 3) () 4 // [1, [2, 3], [], 4]
`literal
number, string, boolean, null and undefined literal are support.`js
j 0 -1 0.1 999 -0 // [0, -1, 0.1, 999, -0]
j "foo" 'bar' // ['foo', 'bar']
j true false null (NaN undefined) // [true, false, null, [NaN, undefined]]
`tsjson does not support lisp-style
nil and t.symbol
other symbol (lisp) are treated encode to string directly.
symbol are strings which contain no space and special character.`js
j symbol1 s2 a-symbol star under_score
// ['symbol1', 's2', 'a-symbol', 'star', 'under_score']
`(most cases, in js, symbol is just string.
we write
addEventListener('click', ...),
where the click is a symbol.)string
string are treated as string.
string can use double quote and single quote.`js
j "abc" 'def ghi' // ['abc', 'def ghi']
`tsj will use the raw string, so the result will be intuitive
when using template literal string.
(this mean that you don't need escape special chars twice,
in backquote string and in tsj string.)
`js
j "a \\ \n \\n \\\n \r \t \" \' \ \${1} ${1}" ${1} 1']/ in raw*:
a \ \n \ " '
${1} 1A newline prefixed with backslash would be ignored:
`js
j "a\
// ['ab']
`variable interpolation
template string allow variable interpolation,
tsj will handle this in a intuitive way.$3
A standalone variable will keep as it was.`js
j a ${'a b'} c // ['a', 'a b', 'c']
j a (${ {n: 3} } 2) ${[null]} // ['a', [{n:3}, 2], [null]]
j ${x=>x2} ${/[a-z]/g} // [x=>x2, /[a-z]/g]$3
when a variable adjoins a symbol or another variable,
they are concated and treat as a single symbol.`js
let x = 'string'
j sym-${x}-end sym${x} ${x}sym
// ['sym-string-end', 'symstring', 'stringsym']let y = 'string with space'
j
${x}${y} ${y}s${y}
// ['stringstring with space', 'sstring with space']
`$3
when a variable is inside a string,
its value is direct concat in the string.
(the variable in string will not get its content unescape.)`js
let s = 'a\\nb'
j "1 ${s} 2" // ["1 a\\nb 2"]
`object
if a array's item are symbols and prefix with colon,
then it is treated as an object.this is an object:
`js
j (:key k :value v) // [{key: 'k', value: 'v'}]
`this contains string but not symbol, so it is not an object:
`js
j (":key" k ":value" v)// [[':key', 'k', ':value', 'v']]
`after a symbol concat string, it is still a symbol,
so key can be a symbol concat variable:
`js
j (:${'foo'} foo) // [{foo: 'foo'}]
j (:key-${'foo'} foo) // [{'key-foo': 'foo'}]
j (${':'}key 3) // [{key: 3}]
`$3
in fact, only the first item's prefix colon is neccessory,
so you can skip the colon after that.`js
j (:key 1 k2 2) // [{key: 1, k2: 2}]
`but if the key is prefix with colon, the colon will get remove.
to add key prefix with colon, write 2 colon:
`js
j (::key 1 ::k2 2) // [{':key': 1, ':k2': 2}]
`$3
if the first item is just a colon, it will get skipped
but not cause a empty string key,
and the list will be treat as object.this feature can produce a empty object.
`js
j (: key 1 k2 2) // [{key: 1, k2: 2}]
j (:) // [{}]
j (':') // [[':']]
`since the top level is automatically wrap,
it can be a object too.
`js
j :key 1 // {key: 1}
j : // {}
j ":" // [":"]
`$3
only the first item need to be a symbol.
you can use string as key in the following keys.`js
j :k1 1 "k2" 2 "k e y 3" 3 // {k1: 1, k2: 2, 'k e y 3': 3}
j : "k 1" 1 "k 2" 2 // {'k 1': 1, 'k 2': 2}
`A string key's prefix colon will not get removed.
only the symbol key's prefix colon is removed.
`js
j : :k1 1 ":k2" 2 // {k1: 1, ':k2': 2}
`$3
both object and array can nest.`js
j :k1 (:k2 2 :a (1 2 3)) :k3 null
// {k1: {k2: 2, a: [1,2,3]}, k3: null}
`splice
the @ can splice the following array, object, or variable,
similar to at-sign ,@ in lisp quasiquote ` macro.the space between
@ and variable is optional.$3
array can be anything iteratable except Map.`js
j 1 2 @ (3 4) 5 // [1, 2, 3, 4, 5]
j 1 2 @(3 (4 5) 6) 7 // [1, 2, 3, [4, 5], 6, 7]
j 1 2 @ ${[3, 4]} 5 // [1,2,3,4,5]
j 1 2 @${[3, 4]} 5 // [1,2,3,4,5]
`$3
object are maps or any other things.`js
j :k 1 @(: k2 2 k3 3) k4 4 // {k:1, k2:2, k3:3, k4:4}
j : k 1 @${{k2:2, k3:3}} k4 4 // {k:1, k2:2, k3:3, k4:4}
`$3
do not splice object out of order,
the key will become value.`js
j :k 1 :k2 @(:k3 3 :k4 4) 2 k5 5
`type conversion
values are convert to string if: 1. it is not string or symbol (js symbol, the
Symbol constructor),
and appear as a object key.
2. it is a variable which concat to a symbol.
3. it is a variable which inside a string.example:
`js
j ${1} x${1} "${1}" // [1, 'x1', '1']j
:${1} v1 ${2} v2 ${Symbol.for('v3')} v3
// {'1': 'v1', '2': 'v2', [Symbol.for('v3')]: 'v3'}
`change bracket
you can use square bracket instead of round bracket
(if you do not want to press shift-9 and shift-0 all the time)`js
tsj.bracket = '[ ]'.split(' ')
j a [b c] [:] // ['a', ['b', 'c'], {}]
`cache
you can enable cache for lexing by tsj.enableCache().
this will save 30% time while parsing a same template string.
(test on the npm run bench.)
the template strings are same if they have same string parts.
for example, these strings are same:`js
tsj.j a ${someVar} (1 2 @${someList}) ${e => 'even function'}
tsj.j a ${var2} (1 2 @${l2.concat(l3)}) ${null}
`but not this:
`js
tsj.j a ${someVar} (1 2 @${someList}) ${e => 'even function'}
tsj.j a ${someVar}${var2} (1 2 @${someList}) ${e => 'even function'}
`the
toJson method has no cached now.sxml
tsjson can produce html element with a [sxml][sxml-wiki] like syntax in browser,
but the s-expression do not use (@ (key value) ...) syntax to
define attributes. we use the colon prefix attribute name (the object syntax):
(:key value :k2 v2)[sxml-wiki]: https://en.wikipedia.org/wiki/SXML
see the example if tldr.
tsj.html will return a document fragment from the s-expression,
or return the element if there is only one element in sxml.$3
mostly like sxml, but string and symbol is mostly identical,
and dom node can show up as variable in s-expression.`js
(element (:attribute-name attribute-value ...)
child)
`#### element
element can be a symbol, string or variable.
if it is symbol, string or a variable contain string,
tsj will create corresponding element.
if it is a node, it will be used directly.
following examples are identical:
(ol), ("ol"), (${'ol'}), (${'o'}l),
or (${document.createElement('ol')})this work too:
`js
(${document.createElement('a')} (:href '..' :target _blank) parent)
`#### attribute
then, attributes in attribute object will assign to the element.
(in browser) if the dom object contain that property,
property value will be assigned directly,
ot it is stringify and assign.
the whole attribute object can be a variable:
`js
(a ${{href: '..', target: '_blank'}} parent)
`following examples are identical:
*
(script (:type application/javascript :src index.js))
* (script (:type "application/javascript" :src "index.js"))
* (script ${{type: 'application/javascript', src: 'index.js'}})
* (script (:type ${'application/javascript'} src index.js))#### children
children can be text, another sxml or a node variable.
these are identical:
(div "text"), (div text), or (div ${document.createTextNode('text')})multiple children are append sequently, without space join.
$3
user can define macro to transform the dom tree.
if node names or attribute names match a macro name,
the macro will be execute.
for nodes, the macro is apply on the list before convert them to nodes.
for attributes, if a macro return anything except undefined,
the attribute is set to that value.#### define a macro
node macro:
`js
domTool['macro:my-div'] = (list) => {
list[0] = 'div'
if (!domTool.isDict(list[1]) || domTool.isNode(list[1])) {
list.splice(1, 0, {})
}
if (!list[1]['class']) list[1]['class'] = []
list[1]['class'].push('my-div')
}
tsj.html (my-div (:class another-class) "some text")
// some text
`attribute macro is prefix with colon, so there will be 2 colons.
`js
// only work in browser
domTool['macro::range'] = (node, k, v, dict) => {
const [min, max, step] = v
const dict = {min, max, step, type: 'number'}
for (const k in dict) domTool.setAttribute(node, k, dict[k])
}
tsj.html (input (:range (0 1 0.1) :name num :value 0))
//
`following are built-in macro:
#### ::id
::id is a attribute macro with additional colon prefix.
we use double colon here to distinct from html id attribute.
and it will produce nothing in output html.if a element has
::id attribute, it will be store
to the context object's corresponding key.
the default context object is tsjson.domTool.context.`js
const menu = tsj.html (menuconst {b1, b2, b3} = tsj.domTool.context
addFancyAnimation(b1)
addDebounce(b3)
`if you want to store in specified object but not use the global object:
`js
const ctx = {}
const menu = tsj.html(ctx) (menuconst {b1, b2, b3} = ctx
addFancyAnimation(b1)
addDebounce(b3)
`this could be useful if there are multiple rendering call
mix in async context.
#### ::call
this attribute will pass the element to the callback function.
`js
const detail = tsj.html
`note that the parent and children are not connected when the
oncreate is called, and only the preceding attributes are set.
#### :class
:class is a simple macro convert array to a space joined class string.
(div (:class (header outfit))) become .#### style
style macro can convert s-expression in style element to css syntax.
a list will convert to a style block.
`js
tsj.html (style
`if selector is a list, it will be join with space.
following item are paired as key-value.
if the value is a list, it is join with space too.
the output:
`html
`if you need comma, square-bracket or other special character,
quote them as string.
like:
(style ("input[name=number]" display block))#### :style
this is a attribute macro for style.
convert:
`
(div (:style
(background black
font-size larger
border (yellow 1px solid))
foo))
`to:
`html
foo
`$3
with event handler:`js
tsj.html (button (:onclick ${() => alert('hello world')})
// with onclick set to the function
`a larger document:
`js
tsj.html
`the [playground.html][playground] is generated from
playground.sxml.js
$3
you can find more macro in lib/macro-more.js.
to use this:`js
import mmacro from '@gholk/tsjson/lib/macro-more.esm.js'
Object.assign(tsj.domTool, mmacro.domTool)
`or (without es module in browser):
`html
`#### script
if a children is a function, use its body as code in script.
`js
tsj.html (script ${() => {
`will output:
`html
`#### macro
define a macro in inline.
`js
tsj.html
`output:
`html
`note that this will set
tsj.domTool['macro:checkbox']
to the function after the sxml is evaluated,
so use this macro carefully.#### do
the do macro just execute the passed function.
`js
let n = 'before'
tsj.html (do ${() => n = 'after'})
console.assert(n == 'after')
`the do macro become a document fragment after evaluation.
if the functions return a non-undefined value,
it will append to the fragment.
so the above code will produce a text node
after$3
To use this feature outside browser or without document object,
you need to overwrite the method tsjson.domTool.* .#### cheerio
A domTool for [cheerio] are included in
lib/cheerio-dom-tool.js.
example:`js
import tsjson from '@gholk/tsjson'
import {domTool as chdt} from '@gholk/tsjson/lib/cheerio-dom-tool.js'
import cheerio from 'cheerio'const domTool = Object.assign(tsjson.domTool, chdt)
const $ = cheerio.load(
)
domTool.setCheerio($)// without doctype cause quirk mode. or just
// domTool.setCheerioModule(cheerio)
const $div = tsjson.html
(div (:id a-div) "i am a div")
console.log(domTool.toHtml($div))$('body').append($div)
console.log($.html())
`note that the event handler and non-string attributes
will not preserve in cheerio.
all attributes are converted to string in cheerio.
[cheerio]: https://cheerio.js.org/
tabular
`js
tsj.jtable // [{name: 'book1', key: 1, summary: 'the book 1'},
// {name: 'book 2', key: 2, summary: 'the book 2'}]
`install
npm install @gholk/tsjson or npm install the tarball,
or just unzip the tarball and require the index.esm.jsto import as es-module, import the
*.esm.js file if possible.
to run in browser without es-module, load the *.browser.js (if exists),
and you will have a tsjson global variable.
the .js is es-module, or common-js module if .esm.js exist.static js file on gitlab:
*
*
*
*
*
*
cli tool
cli tools are in bin.
cli tools accept arguments or read from stdin if no argument.
to enable print non-json value (like function or regexp),
you can install the optional dependency [stringify-object].[stringify-object]: https://www.npmjs.com/package/stringify-object
(to enable stringify-object in [playground.html] ,
install and
npm run build-stro,
or just wget it from online playground.)if the inputs first non-space char is backquote
`,
then it will chop the first and the last non-space chars.
(so a sxml can be a legal js file which contains only a backquote string.)`term
~/tsjson $ bin/tsj.js '1 2 a b ("str1" "str2") (:ok false :val null)'
[
1,
2,
'a',
'b',
['str1', 'str2'],
{ok: false, val: null}
]
`if you need the json output, add the
--json parameter`term
~/tsjson $ bin/tsj.js --json '1 2 a b ("str1" "str2") (:ok false :val null)'
[
1,
2,
"a",
"b",
[
"str1",
"str2"
],
{
"ok": false,
"val": null
}
]
`the
tsj-html.js requires cheerio installed,
and the lib/macro-more.js is import default,
so you can use macro and others macro.
cheerio is a optional dependency for tsjson.`term
~/tsjson $ echo '(div (:id div1)) (div (:id div2))' | bin/tsj-html.js
~/tsjson $ bin/tsj-html.js '(div (:id div1)) (div (:id div2))'
~/tsjson $ bin/tsj-html.js < playground.sxml.js > playground.html
`$3
#### backquote
These commands will ignore the first char and the last char
if they were backquote.#### bracket
It also detect if the input was in square bracket and change the bracket.
#### css
if
tsj-html.js output has only a css element,
it will output the raw css without html entity encoding.`term
~ $ cat style.css.lsp
[css
[blockquote::before
content "'> '"]]
~ $ tsj-html.js < style.css.lspblockquote::before {
content: '> ';
}
`why
Write large json is tedious.
You need comma, colon and quotation marks.For a string array:
['a', 'b', 'c'] contains 15 characters.
For every string in array, it need about 3 addition character,
2 quotes and 1 camma.
why not just qw a b c `?with s-expression, you don't need comma,
the things matter are only spaces and brackets.
[sexp-tokenizer]: https://www.npmjs.com/package/sexp-tokenizer