expexp
Version:
The express model io and express model and data representation.
275 lines (261 loc) • 9.23 kB
JavaScript
// //////////////////////////////////////////////
// Syntax JSON format and tree traverse concept
// This is a description of the concepts, ideas and guidelines behind
// the form and structure of the parsed JSON.
// The tree traverse function depends on that structure. So they are
// described both together here.
//
// The traverse function keeps track and notifies in its events the
// variables: ii, path and json
// - json: Is the current part of the JSON that is in focus at the moment.
// Initially this is the whole JSON.
// - path: It is the chain of parent elements down to the root (the whole JSON)
// When afterFct or beforeFct are called for the current element,
// the path does not contain json.
// - ii: Stands for identifier or index. It names the context of
// the json object: If the parent is semantically an object, the
// type of the top element at ii is string, providing the name of
// the key. If the parent is semantically an array, the type of
// the element is JS number (0 or positive and whole), providing
// the index position.
//
// What does 'is semantically' mean?
// Even though the JSON contains certainly arrays, they always are
// represented as JS objects. This is to provide also a name for
// the type of array. Whether json stands for an array can be checked
// using the isArrSem(json) function. If so, there is - besides
// the 't' only one more key 's' of type JS array holding the actual
// array elements. The point is about having a format for tree unit
// test data and at the same time human readable path positions in
// the debug output. See the *.tlog files in the /test/units or
// test/u* dirs.
//
// Remarks:
// - The length of path and ii is identical.
// - Neither json nor path do at any time contain a JS array.
// Their content or elements are JS objects with a granted key 't'
// of type string.
// - The value of 't' unambiguously defines what other keys must
// be present.
// - Any object with 't':'*_dcl' (but 'sct_dcl') has also a key 'typ':{..} .
// - The 't':'typ' as well as any aggregation type has a key 'spc' defining
// the base type (of the elements) or being a reference.
// - A key 'id' always stands for a JS string but for a JS object
// in the following cases:
// For 't':'a_atr_ids':
// 'id':{t:'atr|qal_atr|qal_atr_rnmd', id:'..', ..} .
// For 't':'drv|inv':
// 'id':{t:'atr|qal_atr|qal_atr_rnmd', id:'..', ..} .
// For 't':'cst_dcl':
// 'id':{t:'cst', id:'..', ..} .
// For 't':'a_prm_ids':
// 'id':{t:'prm', id:'..', ..} .
// For 't':'a_lcl_ids':
// 'id':{t:'lcl', id:'..', ..} .
// - The 'id' key of an operation {'t':'o_*'} is always a string.
// - A key '*[Rr]ef' always stands for a JS object with {t:'*_ref', id:'..'} .
// - A key '*[Rr]efs' always stands for a JS array containing JS
// objects with {t:'*_ref', id:'..'} .
function traverseContent(ii, path, json, afterFct, beforeFct, keyOrder) {
if (Array.isArray(json)) {
// Simple and plane iteration without any event notification.
for (let i=0; i<json.length; i++) {
ii.push(i)
traverseContent(ii, path, json[i], afterFct, beforeFct, keyOrder)
ii.pop() // i
}
} else { // json is object: {t:'..', ..}
beforeFct(json, path, ii)
path.push(json)
// But we have to check for our special array-objects.
if (isArrSem(json) == false) {
// Establish the defined key order for iterating objects.
const keys = Object.keys(json).filter(key => !IGNORE_KEY[key])
if (1 < keys.length) {
// Initially the order was thought to be most-important-first sorted.
// But to improve performance the whole thing is set up reverted.
// We better sort only this short (even filtered) array instead
// of the lengthy order array later.
keys.sort() // alphanumerically
if (Array.isArray(keyOrder) && 0 < keyOrder.length) {
keys.reverse()
// The reverted sorting causes order.length-i later.
const order = keys.concat(keyOrder)
// const order = DEFAULT_OBJECT_KEY_TRAVERSE_ORDER.concat(keys)
// order.reverse()
const key2idx = {}
order.forEach((elt, i) => { key2idx[elt] = order.length-i }) // Index needs not be exact (missing -1).
// A key, that is in the json as well as in the special array,
// gets the lower index of the special array.
keys.sort((a, b) => key2idx[a]-key2idx[b])
}
}
for (const key of keys) {
const value = json[key]
if (value !== null && typeof value == 'object' && Array.isArray(value) == false) {
// Per design we do not have arrays here.
ii.push(key)
traverseContent(ii, path, value, afterFct, beforeFct, keyOrder)
ii.pop() // key
}
}
} else {
// The special array-object is the event arg, and
// already triggered here, while traverseContent(..)
// only handles the true array (value.s).
traverseContent(ii, path, json.s, afterFct, beforeFct, keyOrder)
}
path.pop() // json
afterFct(json, path, ii)
}
}
const II_ROOT = '≡'
const IGNORE_KEY = {'p':true, 't':true}
const DEFAULT_OBJECT_KEY_TRAVERSE_ORDER = [
// For most cases the order is a nice to have, but
// for fct, xpr, itv { < < } and qry( <* | ): order src-var-cnd
// the order fits op stack architecture and is functionally essential.
'arg0',
'qals0',
'arg',
'arg1',
'qals1',
'op',
'id1', // a compare relation op id
'arg2',
'id2', // a compare relation op id
// Unfortunately there are some technical dependencies introduced
// by the fact, that all must fit into one 'flat' order.
// Through cnd(qry) also involved are: ntt and sct_dcl
// Through whrs(ntt) also involved are: typ and rul
// Through csts, lcls, ctts (rul) also: fun and prc
// Through nttRef also involved are: inv, qal_atr, qal_atr_rnmd
// Through ref(qal_atr_rnmd) also: ref_rnmd, enm_xtdd, slc_xtdd
// Through refs(slc_xtdd) also: slc, use, ref, scm
// Through xtd(enm_xtdd, slc_xtdd) also: enm, slc
// Through vals(enm, enm_xtdd) also: agi, bif, coc
'src', // qry
'var', // qry, als, rpt
'id', // ntt, sct_dcl, .. and really many more, but good, this always comes first.
'spc', // typ, and really many more, but good, this comes after 'id' .
'xtd', // enm, enm_xtdd, slc, slc_xtdd
'nttOnly', // slc, slc_xtdd
'agrTyp', // inv
'agrBds', // inv
'nttRef', // sct_dcl, inv, qal_atr, qal_atr_rnmd
'ref', // qal_atr_rnmd, ref_rnmd, enm_xtdd, slc_xtdd, asg, als
'refs', // slc, slc_xtdd, use, t:ref, scm
'qals', // asg, als
'nttRefs', // rul
'invRef', // inv
'vals', // enm, enm_xtdd, agi, bif, coc
'abs', // ntt, sct_dcl
'ttlNttRefs', // sct_dcl
'cnd', // qry, ntt, sct_dcl, iff
'sprs', // ntt
'atrs', // ntt
'drvs', // ntt
'invs', // ntt
'unqs', // ntt
'itr', // rpt
'whl', // rpt
'utl', // rpt
'prms', // fun, prc
'res', // fun
'ctts', // rul, fun, prc (algorithm_head order)
'csts', // rul, scm, fun, prc (algorithm_head order)
'lcls', // rul, fun, prc (algorithm_head order)
'stms', // fun, prc, rul, cpd, als, rpt, iff
'whrs', // ntt, typ, rul
'elss', // iff
'slr', // cas
'caas', // cas
'oth', // cas
'xprs', // caa
'stm', // caa
'fr', // bds, itr
'to', // bds, itr
'by', // itr
'scts' // scm
]
const HUMAN_READ_ORDER = ['t'].concat(DEFAULT_OBJECT_KEY_TRAVERSE_ORDER)
const HUMAN_READ_IDX = {}
HUMAN_READ_ORDER.forEach(key => {
HUMAN_READ_IDX[key] = HUMAN_READ_ORDER.indexOf(key) + 1 // 0 is not good
})
const HUMAN_READ_ORDER_FCT = function(a, b) {
const aIdx = HUMAN_READ_IDX[a] // might be undefined
const bIdx = HUMAN_READ_IDX[b] // might be undefined
if (aIdx && bIdx) {
return aIdx - bIdx
} else if (!aIdx && !bIdx) {
return a.localeCompare(b) // alphanumerical
} else if (aIdx && !bIdx) {
return -1
} else if (!aIdx && bIdx) {
return 1
}
}
const noOp = function(){}
function traverseExternal(json, afterFct, beforeFct=noOp, keyOrder=DEFAULT_OBJECT_KEY_TRAVERSE_ORDER, path=[], ii=[II_ROOT]) {
if (!json) return
if (Array.isArray(keyOrder)) {
keyOrder = keyOrder.slice() // copy
keyOrder.reverse()
}
traverseContent(ii, path, json, afterFct, beforeFct, keyOrder)
}
function isArrSem(json) {
return json && json.t && json.t.startsWith('a_')
}
function path2str(ii, path, json) {
let fullPath = path
if (json) {
fullPath = path.slice()
fullPath.push(json)
}
const strs = fullPath.map(function(elt, i, arr){
let lbl = ii[i]
if (Number.isInteger(lbl)) {
const arrObj = arr[i-1]
const pArrLenStrLength = ` ${arrObj.s.length}`.length
lbl = String(lbl).padStart(pArrLenStrLength, ' ')
lbl = `[${arrObj.t}${lbl}]`
} else {
lbl = '.' + lbl
}
let eltStr // undefined
if (isArrSem(elt)) {
if (i < fullPath.length-1) {
eltStr = ''
} else { // At the top.
if (0 < elt.s.length) {
eltStr = `[${elt.t}:${elt.s.length}]`
} else {
eltStr = `[]`
}
}
} else {
eltStr = elt.t
if (elt.id) {
eltStr += ' ' + elt.id
}
eltStr = `{${eltStr}}`
}
return lbl + eltStr
})
return strs.join('').substr(1)
}
function DEFAULT_ORDER() {
return DEFAULT_OBJECT_KEY_TRAVERSE_ORDER.slice()
}
function ALPHANUM_ORDER() {
return []
}
export {
DEFAULT_ORDER, ALPHANUM_ORDER,
HUMAN_READ_ORDER_FCT,
traverseExternal as traverse,
isArrSem,
path2str
}