sourceafPegger::TreeRules.fan


** Rules and actions for creating tree structures.  
@Js
class TreeRules : Rules {

    ** Creates and pushes a 'TreeItem' onto the tree. 
    Rule push(Str type, Obj? data := null) {
        doAction(pushAction(type, data))
    }
    
    ** Creates and adds a 'TreeItem' onto the tree. 
    Rule add(Str type, Obj? data := null) {
        doAction(addAction(type, data))
    }
    
    ** Pops the last 'TreeItem' off the tree. 
    Rule pop(Str? type := null) {
        doAction(popAction(type))
    }
    
    ** Creates and pushes a 'TreeItem' onto the tree. 
    |Str matched, Obj? ctx| pushAction(Str type, Obj? data := null) {
        |Str matched, TreeCtx ctx| { ctx.push(type, matched, data) }
    }

    ** Creates and adds a 'TreeItem' onto the tree. 
    |Str matched, Obj? ctx| addAction(Str type, Obj? data := null) {
        |Str matched, TreeCtx ctx| { ctx.current.add(type, matched, data) }
    }

    ** Pops the last 'TreeItem' off the tree. 
    |Str matched, Obj? ctx| popAction(Str? type := null) {
        |Str matched, TreeCtx ctx| { ctx.pop(type) }
    }
}

** The 'ctx' object that holds the tree structure. Is created with an initial 'root' element
@Js
class TreeCtx {
    TreeItem[]  items   := TreeItem[TreeItem("root")]

    ** Returns the *current* tree item. This is set by 'push()' and 'pop()'.
    TreeItem current    := items.first
    
    ** Creates a 'TreeItem' adding it as a child and setting it to current.
    This push(Str type, Str? matched := null, Obj? data := null) {
//      echo("pushing $type")
        item := TreeItem(type, matched, data)
        item.parent = current
        current.items = current.items.rw.add(item)
        current = item
        return this
    }

    ** Pops the last 'TreeItem' off the stack. 
    ** If 'type' is non-null then item are repeatedly popped until the last popped item was of the same type. 
    This pop(Str? type := null) {
//      echo("popping ${current.type}")
        // TODO: add some err handling and proper msgs should we not find what we're looking for
        if (type != null)
            while (current.type != type)
                current = current.parent
        current = current.parent
        return this
    }

    ** Returns the root item.
    TreeItem root() {
        if (items.isEmpty)
            throw Err("Too many pops! List is empty.")
        return items.first
    }
    
    ** Prints the tree structure.
    override Str toStr() {
        items.join("\n")
    }
}

** Represents an item in a tree structure.
@Js
class TreeItem {
    ** The child items.
    TreeItem[]  items   := TreeItem#.emptyList
    
    ** The parent item.
    TreeItem?   parent
    
    ** The item type.
    Str         type
    
    ** Any character data matched to this item.
    Str?        matched
    
    ** User data.
    Obj?        data

    internal new make(Str type, Str? matched := null, Obj? data := null) {
        this.type       = type
        this.matched    = matched
        this.data       = data
    }

    ** Returns a sibling 'TreeItem' or 'null' if this is the first item in the list.
    TreeItem? prev() {
        idx := parent?.items?.findIndex { it === this } ?: 0
        return idx == 0 ? null : parent.items.getSafe(idx - 1)
    }

    ** Returns a sibling 'TreeItem' or 'null' if this is the last item in the list.
    TreeItem? next() {
        idx := parent.items.findIndex { it === this }
        return parent.items.getSafe(idx + 1)
    }

    ** Creates and adds a 'TreeItem' to the end of the child items.
    ** Returns the created 'TreeItem'.
    TreeItem add(Str type, Str? matched := null, Obj? data := null) {
        addItem(TreeItem(type, matched, data))
    }

    ** Adds the 'TreeItem' to the end of the child items and sets the parent.
    ** Returns the given 'TreeItem'.
    TreeItem addItem(TreeItem item) {
        item.parent = this
        items = items.rw.add(item)
        return item
    }

    ** Walks this item and it's children, calling the given funcs when entering and exiting.
    Void walk(|TreeItem|? enter, |TreeItem|? exit) {
        enter?.call(this)
        items.each { it.walk(enter, exit) }
        exit?.call(this)
    }
    
    ** Prints the tree structure.
    override Str toStr() {
        str := type + ":" + (data ?: "") + ":" + (matched?.toCode(null) ?: "")
        if (items.size > 0)
            str += "\n" + items.join("\n").splitLines.map { "    ${it}" }.join("\n")
        return str
    }
}