Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 856
James Avatar asked Jan 30 '09 21:01

James


3 Answers

The problem here is that there is not any good way to retrieve the headings in document order. For example the jQuery call $('h1,h2,h3,h4,h5,h6') will return all of your headings, but all <h1>s will come first followed by the <h2>s, and so on. No major frame work yet returns elements in document order when you use comma delimited selectors.

You could overcome this issue by adding a common class to each heading. For example:

<h1 class="heading">Heading level 1</h1>

    <h2 class="heading">Sub heading #1</h2>

    <h2 class="heading">Sub heading #2</h2>

        <h3 class="heading">Sub Sub heading</h3>

    <h2 class="heading">Sub heading #3</h2>

    ...

Now the selector $('.heading') will get them all in order.

Here is how I would do it with jQuery:

var $result = $('<div/>');
var curDepth = 0;

$('h1,h2,h3,h4,h5,h6').addClass('heading');
$('.heading').each(function() {

    var $li = $('<li/>').text($(this).text());

    var depth = parseInt(this.tagName.substring(1));

    if(depth > curDepth) { // going deeper

        $result.append($('<ol/>').append($li));
        $result = $li;

    } else if (depth < curDepth) { // going shallower

        $result.parents('ol:eq(' + (curDepth - depth - 1) + ')').append($li);
        $result = $li;

    } else { // same level

        $result.parent().append($li);
        $result = $li;

    }

    curDepth = depth;

});

$result = $result.parents('ol:last');

// clean up
$('h1,h2,h3,h4,h5,h6').removeClass('heading');

$result should now be your <ol>.

Also, note that this will handle an <h4> followed by an <h1> (moving more than one level down at once), but it will not handle an <h1> followed by an <h4> (more than one level up at a time).

like image 177
Prestaul Avatar answered Oct 07 '22 23:10

Prestaul


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();
like image 21
strager Avatar answered Oct 08 '22 01:10

strager


I can envision many situations where you might be overthinking this. For many situations, you would really only need the appearance of the hierarchy, and not the actual regenerated HTML hierarchy itself, for which you can do something simple like this:

#nav li.h1 { padding: 0 0 0  0px; } #nav li.h1:before { content: 'h1 '; }
#nav li.h2 { padding: 0 0 0 10px; } #nav li.h2:before { content: 'h2 '; }
#nav li.h3 { padding: 0 0 0 20px; } #nav li.h3:before { content: 'h3 '; }
#nav li.h4 { padding: 0 0 0 30px; } #nav li.h4:before { content: 'h4 '; }
#nav li.h5 { padding: 0 0 0 40px; } #nav li.h5:before { content: 'h5 '; }
#nav li.h6 { padding: 0 0 0 50px; } #nav li.h6:before { content: 'h6 '; }

 

for (i=1; i<=6; i++) {
    var headers = document.getElementsByTagName('h'+i);
    for (j=0; j<headers.length; j++) {
        headers[j].className = 'h';
    }
}
var headers = document.getElementsByClassName('h');
var h1 = document.getElementsByTagName('h1')[0];
h1.parentNode.insertBefore(document.createElement('ul'),h1.nextSibling);
h1.nextSibling.id = 'nav';
for (i=0; i<headers.length; i++) {
    document.getElementById('nav').innerHTML += '<li class="'+headers[i].tagName.toLowerCase()+'">'+headers[i].innerHTML+'</li>';
}
like image 30
Jan Kyu Peblik Avatar answered Oct 08 '22 00:10

Jan Kyu Peblik