@mojule/tree-factory
Version:
Takes an adapter/plugins and generates a consistent API over arbitrary tree-like data
426 lines (296 loc) • 9.11 kB
JavaScript
const is = require( '@mojule/is' )
const utils = require( '@mojule/utils' )
const jsonClone = utils.clone
const Common = ( node, state, getState ) => {
const Node = ( rawNode, rawParent, rawRoot ) => node({
root: rawRoot || state.root,
node: rawNode,
parent: rawParent || null
})
// expose raw
const get = () => state.node
const getRoot = () => node( { root: state.root, node: state.root, parent: null } )
// functional
const map = ( TargetTree, valueMapper = value => value ) => {
const value = node.getValue()
const mappedValue = valueMapper( value )
const mappedNode = TargetTree( mappedValue )
node.getChildren().forEach( child => {
const mappedChild = child.map( TargetTree, valueMapper )
mappedNode.add( mappedChild )
})
return mappedNode
}
const clone = () => {
const value = node.getValue()
let clonedValue
try{
clonedValue = jsonClone( value )
} catch( e ){
throw new Error( 'Node value must be JSON serializable to clone' )
}
const rawClone = node.createRawNode( clonedValue )
const clonedNode = Node( rawClone, null, rawClone )
node.getChildren().forEach( child => {
const clonedChild = child.clone()
clonedNode.add( clonedChild )
})
return clonedNode
}
// traversal
const ancestors = () => {
const nodes = []
const parent = node.getParent()
if( parent )
parent.walkUp( current => {
nodes.push( current )
})
return nodes
}
const childAt = index => node.getChildren()[ index ]
const closest = predicate => {
let target
node.walkUp( current => {
if( predicate( current ) ) {
target = current
return true
}
})
return target
}
const descendants = () => node.findAll( current => current !== node )
const firstChild = () => node.getChildren()[ 0 ]
const getParent = () => {
if( state.node === state.root )
return
return Node( state.parent )
}
const hasAncestor = ancestor => {
let has = false
node.walkUp( current => {
if( current === ancestor ){
has = true
return has
}
})
return has
}
const lastChild = () => {
const children = node.getChildren()
return children[ children.length - 1 ]
}
const nextSibling = () => {
const parent = node.getParent()
if( is.undefined( parent ) )
return parent
const children = parent.getChildren()
const index = children.indexOf( node )
return children[ index + 1 ]
}
const previousSibling = () => {
const parent = node.getParent()
if( is.undefined( parent ) )
return parent
const children = parent.getChildren()
const index = children.indexOf( node )
return children[ index - 1 ]
}
const siblings = () => {
if( state.root === state.node )
return []
const parent = node.getParent()
const children = parent.getChildren()
return children.filter( child => child !== node )
}
/*
walk is so important and used so much that it was worth the time to benchmark
and improve, this version beats or equals all of the other versions we tried,
including those from other tree/graph libraries, hence the inconsistency in
coding style compared to the rest of the modules
*/
const walk = callback => {
let current, parent, depth, i, children, stop
const nodes = [ node ]
const parents = [ null ]
const depths = [ 0 ]
while( nodes.length ){
current = nodes.pop()
parent = parents.pop()
depth = depths.pop()
stop = callback( current, parent, depth )
if( stop ) break
children = current.getChildren()
for( i = children.length - 1; i >= 0; i-- ){
nodes.push( children[ i ] )
parents.push( current )
depths.push( depth + 1 )
}
}
return stop
}
const walkUp = callback => {
let stop = callback( node )
if( stop ) return
let parent = node.getParent()
while( parent && !stop ){
stop = callback( parent )
if( !stop )
parent = parent.getParent()
}
return stop
}
// query
/*
stub - child argument is so you can test for specific nodes only accepting
certain children, default behaviour is just not parent is empty
*/
const accepts = child => !node.isEmpty()
const atPath = ( path, separator = '/' ) => {
let node = getRoot()
const slugs = path.split( separator ).filter( s => s !== '' )
slugs.forEach( slug => {
if( is.undefined( node ) )
return
const children = node.getChildren()
node = children.find( child =>
child.slug() === slug
)
})
return node
}
const contains = predicate => !!node.find( predicate )
const find = predicate => {
let target
node.walk( current => {
if( predicate( current ) ) {
target = current
return true
}
})
return target
}
const findAll = predicate => {
const nodes = []
node.walk( current => {
if( predicate( current ) ){
nodes.push( current )
}
})
return nodes
}
const getPath = ( separator = '/' ) => {
if( state.root === state.node ) return separator
const slugs = []
node.walkUp( current => {
const slug = current.slug()
if( slug.includes( separator ) )
throw new Error(
`Node slugs should not contain the separator string "${ separator }"`
)
slugs.unshift( slug )
})
return slugs.join( separator )
}
const hasChild = child => node.getChildren().includes( child )
const hasChildren = () => node.getChildren().length > 0
const index = () => {
if( state.root === state.node ) return
const parent = node.getParent()
const children = parent.getChildren()
return children.indexOf( node )
}
/*
stub - default nodes are never empty nodes (empty in the HTML sense, cannot
accept children, not does not have children)
*/
const isEmpty = () => false
const metadata = {}
const getMeta = name => metadata[ name ]
const nodeType = () => 'node'
const setMeta = ( name, value ) => metadata[ name ] = value
const meta = ( name, value ) => {
if( is.undefined( name ) )
return metadata
if( is.object( name ) )
return Object.assign( metadata, name )
if( !is.string( name ) )
throw new Error(
'Expected an object or a string as first argument to meta'
)
if( is.undefined( value ) )
return node.getMeta( name )
return node.setMeta( name, value )
}
const slug = () => state.root === state.node ? '' : String( node.index() )
// manipulation
const append = child => node.add( child )
const insertBefore = ( child, reference ) => node.add( child, reference )
const empty = () => node.getChildren().map( child => node.remove( child ) )
const insertAfter = ( child, reference ) => {
const children = node.getChildren()
const index = children.indexOf( reference )
const after = children[ index + 1 ]
return insertBefore( child, after )
}
const insertAt = ( child, index ) => {
const children = node.getChildren()
const reference = children[ index ]
return insertBefore( child, reference )
}
const prepend = child => {
const children = node.getChildren()
// if child[ 0 ] is undefined this is the same as append
return insertBefore( child, children[ 0 ] )
}
const prune = predicate => {
const removed = []
node.walk( current => {
if( predicate( current ) )
removed.push( current.remove() )
})
return removed
}
const removeAt = index => {
const children = node.getChildren()
const child = children[ index ]
return node.remove( child )
}
const replaceChild = ( child, old ) => {
insertBefore( child, old )
return node.remove( old )
}
const unwrap = () => {
if( state.root === state.node )
throw new Error( 'Cannot unwrap root node' )
const parent = node.getParent()
const children = node.getChildren()
children.forEach( child =>
parent.insertBefore( child, node )
)
return parent.remove( node )
}
const value = newValue => {
if( is.undefined( newValue ) )
return node.getValue()
return node.setValue( newValue )
}
const wrap = wrapper => {
const parent = node.getParent()
if( parent )
parent.insertBefore( wrapper, node )
return wrapper.append( node )
}
return {
get, getRoot,
map, clone,
ancestors, childAt, closest, descendants, firstChild, getParent,
hasAncestor, lastChild, nextSibling, previousSibling, siblings, walk,
walkUp,
accepts, atPath, contains, find, findAll, getMeta, getPath, hasChild,
hasChildren, index, isEmpty, meta, nodeType, setMeta, slug,
append, empty, insertAfter, insertAt, insertBefore, prepend, prune,
removeAt, replaceChild, unwrap, value, wrap
}
}
module.exports = Common