JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • 0
  • Score
    100M100P100Q34633F
  • License AGPL-3.0-or-later

A Template literal converts s-expression to json which support variable embedding.

Package Exports

  • @gholk/tsjson
  • @gholk/tsjson/es-module.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@gholk/tsjson) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Template S-expression to JSON

A Template literal converts s-expression to json which support variable embedding, include a sxml like mini template engine.

usage

import tsj from '@gholk/tsjson' // or 'path/to/tsjson/es-module.js'
const j = tsj.j

const 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"]

array

The whole s-expression is wrap in brackets automatically.

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.

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.

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.

j `"abc" 'def ghi'` // ['abc', 'def ghi']

string support basic backslash escape sequence.

j `'\\r \\n \\t \\_ \\\\ \' \\\' " \\" \\\\n'`
// ['\r \n \t \\_ \\ \' \\\' " " \\\n']

/* in raw:
   \r \n \t \_ \\ ' \' " \" \\n
   produce
   \r \n \t \_ \ ' \' " " \\n
*/

variable interpolation

template string allow variable interpolation, tsj will handle this in a intuitive way.

variable standalone

A standalone variable will keep as it was.

j `a ${'a b'} c` // ['a', 'a b', 'c']
j `a (${ {n: 3} } 2) ${[null]}` // ['a', [{n:3}, 2], [null]]
j `${x=>x*2} ${/[a-z]/g}` // [x=>x*2, /[a-z]/g]

variable concat symbol

when a variable adjoins a symbol or another variable, they are concated and treat as a single symbol.

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']

variable inside string

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.)

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:

j `(:key k :value v)` // [{key: 'k', value: 'v'}]

this contains string but not symbol, so it is not an object:

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:

j `(:${'foo'} foo)` // [{foo: 'foo'}]
j `(:key-${'foo'} foo)` // [{'key-foo': 'foo'}]
j `(${':'}key 3)` // [{key: 3}]

colon omission

in fact, only the first item's prefix colon is neccessory, so you can skip the colon after that.

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:

j `(::key 1 ::k2 2)` // [{':key': 1, ':k2': 2}]

colon only key

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.

j `(: key 1 k2 2)` // [{key: 1, k2: 2}]
j `(:)` // [{}]
j `(':')` // [[':']]

since the top level is automatically wrap, it can be a object too.

j `:key 1` // {key: 1}
j `:` // {}
j `":"` // [":"]

string key

only the first item need to be a symbol. you can use string as key in the following keys.

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.

j `: :k1 1 ":k2" 2` // {k1: 1, ':k2': 2}

nest

both object and array can nest.

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.

array

array can be anything iteratable except Map.

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]

object

object are maps or any other things.

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}

undefined behavior

do not splice object out of order, the key will become value.

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:

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)

tsj.bracket = '[ ]'.split(' ')
j `a [b c] [:]` // ['a', ['b', 'c'], {}]

sxml

tsjson can produce html element with a sxml 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 dict syntax): (:key value :k2 v2)

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.

syntax

mostly like sxml, but string and symbol is mostly identical, and dom node can show up as variable in s-expression.

(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:

(${document.createElement('a')} (:href '..' :target _blank) parent)

attribute

then, attributes in attribute dict 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 dict can be a variable:

(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))

special attribute

following special attributes are handled by the tsjson, and will not output. this feature is in experiment, the name may change in future.

::id

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.

const menu = tsj.html `(menu
  (li (button (::id b1 :onclick ${handleClick}) b1))
  (li (button (::id b2 :onclick ${handleClick}) b2))
  (li (button (::id b3 :onclick ${handleClick}) b3)))`

const {b1, b2, b3} = tsj.domTool.context
addFancyAnimation(b1)
addDebounce(b3)

if you want to store in specified object but not use the global object:

const ctx = {}
const menu = tsj.html(ctx) `(menu
  (li (button (::id b1 :onclick ${handleClick}) b1))
  (li (button (::id b2 :onclick ${handleClick}) b2))
  (li (button (::id b3 :onclick ${handleClick}) b3)))`

const {b1, b2, b3} = ctx
addFancyAnimation(b1)
addDebounce(b3)

this could be useful if there are multiple rendering call mix in async context.

::call

this special attribute will pass the element to the callback function.

const detail = tsj.html `
  (details
   (summary (::call ${addFancyAnimation}) 'open me')
   "i am open")`

note that the parent and children are not connected when the oncreate is called, and only the preceding attributes are set.

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.

sxml example

with event handler:

tsj.html `(button (:onclick ${() => alert('hello world')})
           "hello world ${n}!")`
// <button>hello world ${n}!</button>with onclick set to the function

a larger document:

tsj.html `
(html
 (head
  (style "body { background: black; color: white; }")
  (script (:src index.js :type application/javascript))
  (title index))
 (body
  (h1 "a index html")
  (p "hey user ${user}")
  (p "go "
     (a (:href "..") "back"))
  (script "alert('welcome to index!')")))`

non-browser environment

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:

import tsjson from 'tsjson/es-module.js'
import {domTool} from 'tsjson/lib/cheerio-dom-tool.js'
tsjson.domTool = domTool

import cheerio from 'cheerio'

const $ = cheerio.load('<html><body></body></html>')
domTool.setCheerio($)

// 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.

tabular

tsj.jtable `
(name key summary)
book1    1 'the book 1'
'book 2' 2 'the book 2'`
// [{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.js

to run in browser, load the browser.js, and you will have a tsjson global variable.

cli tool

cli tools are in bin. cli tools accept arguments or read from stdin if no argument. the tsj-html.js requires cheerio installed.

~/tsjson $ bin/tsj-html.js '(div (:id div1)) (div (:id div2))'
<div id="div1"></div><div id="div2"></div>

~/tsjson $ echo '(div (:id div1)) (div (:id div2))' | bin/tsj-html.js
<div id="div1"></div><div id="div2"></div>

~/tsjson $ bin/tsj.js '(div (:id div1)) (div (:id div2))'
[
  [
    "div",
    {
      "id": "div1"
    }
  ],
  [
    "div",
    {
      "id": "div2"
    }
  ]
]

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.

license

this project is fork from the [sexp-tokenizer], and re-license as AGPL3+.

todo

  • find a better name and publish in global scope.
  • unzip table to dict, reuse code
  • use and extend painless error to sum type (styp)
  • cache template string lex or parse result and allow insert variable.
  • handle style as special form like: (style (body (color white) (background black))) (style (body p html (:color white :background black)))