Search code examples
javascripthtmlmethodology

Produce heading hierarchy as ordered list


I've been pondering this for a while but cannot come up with a working solution. I can't even psuedo code it...

Say, for example, you have a page with a heading structure like this:

<h1>Heading level 1</h1>

    <h2>Sub heading #1</h2>

    <h2>Sub heading #2</h2>

        <h3>Sub Sub heading</h3>

    <h2>Sub heading #3</h2>

        <h3>Sub Sub heading #1</h3>

        <h3>Sub Sub heading #2</h3>

            <h4>Sub Sub Sub heading</h4>

    <h2>Sub heading #4</h2>

        <h3>Sub Sub heading</h3>

Using JavaScript (any framework is fine), how would you go about producing a list like this: (with nested lists)

<ol>
    <li>Heading level 1
        <ol>
            <li>Sub heading #1</li>
            <li>Sub heading #2
                <ol>
                    <li>Sub Sub heading</li>
                </ol>
            </li>
            <li>Sub heading #3
                <ol>
                    <li>Sub Sub heading #1</li>
                    <li>Sub Sub heading #2
                        <ol>
                            <li>Sub Sub Sub heading (h4)</li>
                        </ol>
                    </li>
                </ol>
            </li>
            <li>Sub heading #4
                <ol>
                    <li>Sub Sub heading</li>
                </ol>
            </li>
        </ol>
    </li>
</ol>

Everytime I try and begin with a certain methodology it ends up getting very bloated.

The solution needs to traverse each heading and put it into its appropriate nested list - I keep repeating this to myself but I can't sketch out anything!

Even if you have a methodology in your head but haven't got time to code it up I'd still like to know it! :)

Thank you!


Solution

  • First, build a tree. Pseudocode (because I'm not fluent in Javascript):

    var headings = array(...);
    var treeLevels = array();
    var treeRoots = array();
    
    foreach(headings as heading) {
        if(heading.level == treeLevels.length) {
            /* Adjacent siblings. */
    
            if(heading.level == 1) {
                treeRoots[] = heading;  // Append.
            } else {
                treeLevels[treeLevels.length - 2].children[] = heading;  // Add child to parent element.
            }
    
            treeLevels[treeLevels.length - 1] = heading;
        } else if(heading.level > treeLevels.length) {
            /* Child. */
    
            while(heading.level - 1 > treeLevels.length) {
                /* Create dummy headings if needed. */
                treeLevels[] = new Heading();
            }
    
            treeLevels[] = heading;
        } else {
            /* Child of ancestor. */
    
            treeLevels.remove(heading.level, treeLevels.length - 1);
    
            treeLevels[treeLevels.length - 1].children[] = heading;
            treeLevels[] = heading;
        }
    }
    

    Next, we transverse it, building the list.

    function buildList(root) {
        var li = new LI(root.text);
    
        if(root.children.length) {
            var subUl = new UL();
            li.children[] = subUl;
    
            foreach(root.children as child) {
                subUl.children[] = buildList(child);
            }
        }
    
        return li; 
    }
    

    Finally, insert the LI returned by buildList into a UL for each treeRoots.

    In jQuery, you can fetch header elements in order as such:

    var headers = $('*').filter(function() {
        return this.tagName.match(/h\d/i);
    }).get();