views:

530

answers:

2

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!

+1  A: 

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();
strager
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 document order.
Prestaul
@Prestaul, I have added a jQuery snippet to show how you would do this. Thanks for mentioning it.
strager
@strager, that is a nice work around. I just posted a solution that (with jQuery) gets the list by first adding a common class to the headings: $('h1,h2,h3,h4,h5,h6').addClass('heading'); var $aiches = $('.heading');
Prestaul
+1  A: 

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('li: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).

Prestaul