patrun.ts
/* Copyright (c) 2013-2022 Richard Rodger, MIT License */
// TODO: matchers should accept string[] of key names - only operate on these keys
// TODO: expose walk as method for general purpose
import { Gex } from 'gex'
import {
Matcher,
MatchValue,
GexMatcher,
IntervalMatcher,
} from './lib/matchers'
function Patrun(custom?: any) {
custom = custom || {}
var self: any = {}
var top: any = {}
let matchers: Matcher[] = []
if (custom.gex) {
matchers.push(new GexMatcher())
}
if (custom.interval) {
matchers.push(new IntervalMatcher())
}
// Provide internal search order structure
self.top = function () {
return top
}
self.add = function (pat: any, data: any) {
pat = { ...pat }
var customizer =
'function' === typeof custom ? custom.call(self, pat, data) : null
var keys = Object.keys(pat)
.filter((key) => null != pat[key])
.sort()
keys.forEach(function (key) {
pat[key] = String(pat[key])
})
var keymap: any = top
var valmap: any
// Partial matches return next wider match - see partial-match test.
// Traverse the key path (keys are ordered), insert preserves order.
for (var i = 0; i < keys.length; i++) {
// console.log('L', i, keys.length)
var key = keys[i]
var fix = pat[key]
let mv: MatchValue | undefined = matchers.reduce(
(m, t) => m || t.make(key, fix),
undefined,
)
// if (mv) mv.val$ = fix
valmap = keymap.v
// console.log('S0',key,fix,keymap,valmap)
// An existing key
if (valmap && key == keymap.k) {
// console.log('S1-a')
// add_mv(keymap, key, mv)
if (mv) {
var g = (keymap.g = keymap.g || {})
var ga = (g[key] = g[key] || [])
mv = (ga.find((gmv: MatchValue) => gmv.same(mv)) ||
(ga.push(mv), mv)) as MatchValue
keymap = mv.keymap || (mv.keymap = {})
} else {
keymap = valmap[fix] || (valmap[fix] = {})
}
}
// End of key path reached, so this is a new key, ordered last
else if (!keymap.k) {
keymap.k = key
keymap.v = {}
if (mv) {
var g = (keymap.g = keymap.g || {})
var ga = (g[key] = g[key] || [])
mv = (ga.find((gmv: MatchValue) => gmv.same(mv)) ||
(ga.push(mv), mv)) as MatchValue
keymap = mv.keymap || (mv.keymap = {})
} else {
keymap = keymap.v[fix] = {}
}
}
// Insert key orders before next existing key in path, so insert
else if (key < keymap.k) {
// console.log('S1-c', key, keymap.k)
var s = keymap.s
var g = keymap.g
keymap.s = {
k: keymap.k,
// sk: keymap.sk,
v: keymap.v,
}
if (s) {
keymap.s.s = s
}
if (g) {
keymap.s.g = g
}
if (keymap.g) {
keymap.g = {}
}
keymap.k = key
keymap.v = {}
if (mv) {
var g = (keymap.g = keymap.g || {})
var ga = (g[key] = g[key] || [])
mv = (ga.find((gmv: MatchValue) => gmv.same(mv)) ||
(ga.push(mv), mv)) as MatchValue
keymap = mv.keymap || (mv.keymap = {})
} else {
keymap = keymap.v[fix] = {}
}
}
// Follow star path
else {
keymap = keymap.s || (keymap.s = {})
// NOTE: current key is still not inserted
i--
}
}
if (void 0 !== data && keymap) {
keymap.d = data
if (customizer) {
keymap.f =
'function' === typeof customizer ? customizer : customizer.find
keymap.r =
'function' === typeof customizer.remove ? customizer.remove : void 0
}
}
return self
}
self.findexact = function (pat: any) {
return self.find(pat, true)
}
self.find = function (pat: any, exact: any, collect: any) {
if (null == pat) return null
var keymap: any = top
var data: any = void 0 === top.d ? null : top.d
var finalfind = top.f
var key = null
var stars = []
var foundkeys: any = {}
var patlen = Object.keys(pat).length
var collection = []
if (void 0 !== top.d) {
collection.push(top.d)
}
do {
key = keymap.k
if (keymap.v) {
var val = pat[key]
// Matching operation is either string equality (by prop lookup)
// or gex match.
var nextkeymap = keymap.v[val]
if (!nextkeymap && keymap.g && keymap.g[key]) {
var ga = keymap.g[key]
for (var gi = 0; gi < ga.length; gi++) {
if (ga[gi].match(val)) {
nextkeymap = ga[gi].keymap
break
}
}
}
if (nextkeymap) {
foundkeys[key] = true
if (keymap.s) {
stars.push(keymap.s)
}
data = void 0 === nextkeymap.d ? (exact ? null : data) : nextkeymap.d
if (collect && void 0 !== nextkeymap.d) {
collection.push(nextkeymap.d)
}
finalfind = nextkeymap.f
keymap = nextkeymap
}
// no match found for this value, follow star trail
else {
keymap = keymap.s
}
} else {
keymap = null
}
if (
null == keymap &&
0 < stars.length &&
(null == data || (collect && !exact))
) {
keymap = stars.pop()
}
} while (keymap)
if (exact) {
if (Object.keys(foundkeys).length !== patlen) {
data = null
}
} else {
// If there's root data, return as a catch all
if (null == data && void 0 !== top.d) {
data = top.d
}
}
if (finalfind) {
data = finalfind.call(self, pat, data)
}
return collect ? collection : data
}
self.remove = function (pat: any) {
var keymap = top
var data = null
var key
var path = []
do {
key = keymap.k
// console.log('keymap v g', keymap.v, keymap.g)
if (keymap.v || keymap.g) {
if (keymap.v) {
var nextkeymap = keymap.v[pat[key]]
if (nextkeymap) {
path.push({ km: keymap, v: pat[key] })
}
}
if (null == nextkeymap && keymap.g) {
let mvs: MatchValue[] = keymap.g[key] || []
for (let mvi = 0; mvi < mvs.length; mvi++) {
// TODO: should parse!
if (mvs[mvi].fix === pat[key]) {
path.push({ km: keymap, v: pat[key], mv: mvs[mvi] })
nextkeymap = mvs[mvi].keymap
break
}
}
}
if (nextkeymap) {
data = nextkeymap.d
keymap = nextkeymap
} else {
keymap = keymap.s
}
} else {
keymap = null
}
} while (keymap)
if (void 0 !== data) {
var part = path[path.length - 1]
if (part && part.km && part.km.v) {
var point = part.km.v[part.v] || (part.mv && part.mv.keymap)
if (point && (!point.r || point.r(pat, point.d))) {
delete point.d
}
}
}
}
// values can be verbatim, glob, or array of globs
self.list = function (pat: any, exact: boolean) {
pat = pat || {}
function descend(keymap: any, match: any, missing: any, acc: any) {
if (keymap.v) {
var key = keymap.k
var gexval = Gex(
pat ? (null == pat[key] ? (exact ? null : '*') : pat[key]) : '*',
)
var itermatch = { ...match }
var itermissing = { ...missing }
var nextkeymap
for (var val in keymap.v) {
if (
val === pat[key] ||
(!exact && null == pat[key]) ||
gexval.on(val)
) {
var valitermatch = { ...itermatch }
valitermatch[key] = val
var valitermissing = { ...itermissing }
delete valitermissing[key]
nextkeymap = keymap.v[val]
if (
0 === Object.keys(valitermissing).length &&
nextkeymap &&
nextkeymap.d
) {
acc.push({
match: valitermatch,
data: nextkeymap.d,
find: nextkeymap.f,
})
}
if (nextkeymap && null != nextkeymap.v) {
descend(
nextkeymap,
{ ...valitermatch },
{ ...valitermissing },
acc,
)
}
}
}
nextkeymap = keymap.s
if (nextkeymap) {
descend(nextkeymap, { ...itermatch }, { ...itermissing }, acc)
}
}
}
var acc = []
if (top.d) {
acc.push({
match: {},
data: top.d,
find: top.f,
})
}
descend(top, {}, { ...pat }, acc)
return acc
}
self.toString = function (first: any, second: any) {
var tree = true === first ? true : !!second
var dstr =
'function' === typeof first
? first
: function (d: any) {
return 'function' === typeof d ? '<' + d.name + '>' : '<' + d + '>'
}
function indent(o: any, d: any) {
for (var i = 0; i < d; i++) {
o.push(' ')
}
}
var str: any[] = []
function walk(n: any, o: any, d: any, vs: any) {
var vsc
if (void 0 !== n.d) {
o.push(' ' + dstr(n.d))
str.push(vs.join(', ') + ' -> ' + dstr(n.d))
}
if (n.k) {
o.push('\n')
indent(o, d)
o.push(n.k + ':')
}
if (n.v || n.s || n.g) {
d++
}
if (n.v) {
// d++
var pa = Object.keys(n.v).sort()
for (var pi = 0; pi < pa.length; pi++) {
var p = pa[pi]
o.push('\n')
indent(o, d)
o.push(p + ' ->')
vsc = vs.slice()
vsc.push(n.k + '=' + p)
walk(n.v[p], o, d + 1, vsc)
}
}
if (n.g) {
var pa = Object.keys(n.g).sort()
for (var pi = 0; pi < pa.length; pi++) {
var mvs = n.g[pa[pi]]
for (var mvi = 0; mvi < mvs.length; mvi++) {
var mv = mvs[mvi]
o.push('\n')
indent(o, d)
o.push(mv.fix + ' ~>')
vsc = vs.slice()
vsc.push(n.k + '~' + mv.fix)
walk(mv.keymap, o, d + 1, vsc)
}
}
}
if (n.s) {
o.push('\n')
indent(o, d)
o.push('|')
vsc = vs.slice()
walk(n.s, o, d + 1, vsc)
}
}
var o: any = []
walk(top, o, 0, [])
return tree ? o.join('') : str.join('\n')
}
self.inspect = self.toString
self.toJSON = function (indent: any) {
return JSON.stringify(
top,
function (_key: any, val: any) {
if ('function' === typeof val) return '[Function]'
return val
},
indent,
)
}
return self
}
type PatrunRouter = ReturnType<typeof Patrun>
function make(custom?: any) {
return new (Patrun as any)(custom)
}
if ('undefined' !== typeof module) {
module.exports = make
module.exports.Patrun = Patrun
module.exports.Gex = Gex
}
export default make
export { Patrun, Gex }
export type { PatrunRouter }