How to firmly parse a document for any headings and build a <ul> tree of only these headings
So, I am parsing a document to grab all the headers with stackHeadings (). I am doing this to create a Word Word-style document map using buildNav (). Currently it works fine, but it is not very reliable and is interrupted at any time when the headers do not meet strict requirements ... for example. (If you start with H2, it breaks, if you nest H3 under and H1 breaks, etc.)
I cannot figure out how best to fix this (make it more reliable). I use the jQuery `nextUntil 'function to find all h2s between two h1s.
One opportunity replaces:
elem.nextUntil( 'h' + cur, 'h' + next ) with
elem.nextUntil( 'h' + cur, 'h' + next + ',h' + (next + 1) + ',h' + (next + 2) ... ) to find ALL subheadings between two headings of the same level. But now h3 h1s children will only be nested one level, not two.
So, you need to compare the current course level with the parent heading level, and if there is more than one jump (h1 β h3), you need to create an empty child element between them as a nested place for the missing h2.
Any ideas or solutions are welcome!
stackHeadings = (items, cur, counter) -> cur = 1 if cur == undefined counter ?= 1 next = cur + 1 for elem, index in items elem = $(elem) children = filterHeadlines( elem.nextUntil( 'h' + cur, 'h' + next ) ) d.children = stackHeadings( children, next, counter ) if children.length > 0 d filterHeadlines = ( $hs ) -> _.filter( $hs, ( h ) -> $(h).text().match(/[^\s]/) ) buildNav = ( ul, items ) -> for child, index in items li = $( "<li>" ) $( ul ).append( li ) $a = $("<a/>") $a.attr( "id", "nav-title-" + child.id ) li.append( $a ) if child.children subUl = document.createElement( 'ul' ) li.append( subUl ) buildNav( subUl, child.children ) items = stackHeadings( filterHeadlines( source.find( 'h1' ) ) ) ul = $('<ul>') buildNav( ul, items) I put together some JavaScript that will do what you want http://jsfiddle.net/fA4EW/
This is a fairly simple recursive function that consumes an array of elements (nodes) and accordingly builds a UL structure. To fit this question, I add placeholder list items (empty) when you are from H1 to H3, etc.
function buildRec(nodes, elm, lv) { var node; // filter do { node = nodes.shift(); } while(node && !(/^h[123456]$/i.test(node.tagName))); // process the next node if(node) { var ul, li, cnt; var curLv = parseInt(node.tagName.substring(1)); if(curLv == lv) { // same level append an il cnt = 0; } else if(curLv < lv) { // walk up then append il cnt = 0; do { elm = elm.parentNode.parentNode; cnt--; } while(cnt > (curLv - lv)); } else if(curLv > lv) { // create children then append il cnt = 0; do { li = elm.lastChild; if(li == null) li = elm.appendChild(document.createElement("li")); elm = li.appendChild(document.createElement("ul")); cnt++; } while(cnt < (curLv - lv)); } li = elm.appendChild(document.createElement("li")); // replace the next line with archor tags or whatever you want li.innerHTML = node.innerHTML; // recursive call buildRec(nodes, elm, lv + cnt); } } // example usage var all = document.getElementById("content").getElementsByTagName("*"); var nodes = []; for(var i = all.length; i--; nodes.unshift(all[i])); var result = document.createElement("ul"); buildRec(nodes, result, 1); document.getElementById("outp").appendChild(result); You can simply use the jQuery TOC plugin , it seems they do it this way:
$('h1,h2,h3').each(function(i, heading) { ... } Of course, this just applies to all h1 , h2 and h3 evenly and creates a TOC, considering only the order of attachment and the order of documents of only the elements. But is this not the desired behavior?
It would be strange to have h3 , which is directly inside h1 , with double indentation in the TOC. If you cannot live with this inconsistency, instead of inserting the missing h2 between them, I would consider clearing HTML and converting h3 to h2 .
JQuery + Coffescript solution based on @LastCoder answer http://jsfiddle.net/Sydky/1/
buildRec = (headingNodes, $elm, lv) -> # each time through recursive function pull a piece of the jQuery object off node = headingNodes.splice(0,1) if node && node.length > 0 curLv = parseInt(node[0].tagName.substring(1)) if curLv is lv # same level append an il cnt = 0 else if curLv < lv # walk up then append il cnt = 0 loop $elm = $elm.parent().parent() cnt-- break unless cnt > (curLv - lv) else if curLv > lv # create children then append li cnt = 0 loop li = $elm.children().last() # if there are already li at this level if ($elm.children().last().length == 0) li = $("<li>").appendTo($elm); $elm = $("<ul>").appendTo(li); cnt++ break unless cnt < (curLv - lv) li = $("<li>").appendTo($elm); li.text(node[0].innerText); # recursive call buildRec headingNodes, $elm, lv + cnt Using
headingNodes = $('#entry').children().filter(":header") result = $('<ul>') buildRec(headingNodes,result,1) result.html()