"use strict" ;
const assureArray = require ( "assure-array" ) ;
const shallowMerge = require ( "../shallow-merge" ) ;
const syncpipe = require ( "syncpipe" ) ;
const { validateArguments } = require ( "@validatem/core" ) ;
const required = require ( "@validatem/required" ) ;
const isString = require ( "@validatem/is-string" ) ;
const isArray = require ( "@validatem/is-array" ) ;
const ValidationError = require ( "@validatem/error" ) ;
function createListValidator ( ) {
let lastSequenceNumber = null ;
return function isTreecutterList ( value ) {
isArray ( value ) ;
if ( value . some ( ( item ) => item . _treecutterDepth == null || item . _treecutterSequenceNumber == null ) ) {
throw new ValidationError ( ` Must be a treecutter-generated list of items ` ) ;
} else if ( lastSequenceNumber != null && value . _treecutterSequenceNumber !== lastSequenceNumber + 1 ) {
throw new ValidationError ( ` Must be the original, unfiltered, unsorted treecutter-generated list of items ` ) ;
} else {
lastSequenceNumber = value . _treecutterSequenceNumber ;
}
} ;
}
let validateTreecutterOptions = {
childrenProperty : isString
} ;
function defaultOptions ( options = { } ) {
return {
childrenProperty : options . childrenProperty ? ? "children"
} ;
}
module . exports = {
flatten : function ( tree , options ) {
validateArguments ( arguments , {
tree : [ required ] ,
options : [ validateTreecutterOptions ]
} ) ;
let { childrenProperty } = defaultOptions ( options ) ;
let rootItems = assureArray ( tree ) ;
let list = [ ] ;
let sequenceNumber = 0 ;
function add ( items , depth ) {
for ( let item of items ) {
let listItem = shallowMerge ( item , {
_treecutterDepth : depth ,
_treecutterSequenceNumber : sequenceNumber
} ) ;
// listItem is a copy, so we can do this safely
delete listItem [ childrenProperty ] ;
list . push ( listItem ) ;
sequenceNumber += 1 ;
if ( item [ childrenProperty ] != null ) {
add ( item [ childrenProperty ] , depth + 1 ) ;
}
}
}
add ( rootItems , 0 ) ;
return list ;
} ,
rebuild : function ( list , options ) {
let isTreecutterList = createListValidator ( ) ;
validateArguments ( arguments , {
list : [ required , isTreecutterList ] ,
options : [ validateTreecutterOptions ]
} ) ;
let { childrenProperty } = defaultOptions ( options ) ;
let topLevel = [ ] ;
let stack = [ ] ;
let currentDepth = list [ 0 ] ? . _treecutterDepth ;
for ( let item of list ) {
let depth = item . _treecutterDepth ;
let treeItem = shallowMerge ( item , {
[ childrenProperty ] : [ ]
} ) ;
// Again, we're operating on a copy.
delete treeItem . _treecutterDepth ;
delete treeItem . _treecutterSequenceNumber ;
if ( depth >= 0 && depth <= currentDepth + 1 ) {
if ( depth === 0 ) {
topLevel . push ( treeItem ) ;
} else {
stack [ depth - 1 ] [ childrenProperty ] . push ( treeItem ) ;
}
currentDepth = depth ;
stack [ depth ] = treeItem ;
stack . splice ( depth + 1 ) ; // Remove references higher in the stack, to decrease the chance of a silent failure if there's a bug in the code
} else {
throw new Error ( ` Encountered an invalid item depth; the item's depth is ${ depth } , but the current tree depth is ${ currentDepth } ; if this list was generated by treecutter, please file a bug! ` ) ;
}
}
return topLevel ;
} ,
map : function ( tree , mapFunc ) {
return syncpipe ( tree , [
( _ ) => this . flatten ( _ ) ,
( _ ) => _ . map ( ( item ) => ( {
... mapFunc ( item ) ,
_treecutterDepth : item . _treecutterDepth ,
_treecutterSequenceNumber : item . _treecutterSequenceNumber ,
} ) ) ,
( _ ) => this . rebuild ( _ )
] ) ;
}
} ;