Get all headers and resursively create a tree

Viewed 176

I want to create a tree using headers.

Example:

<h1>Wow</h1>
<h2>Blablablub</h2>
<p>Lorem Ipsum...</p>
<h1>Lalalala</h1>
<p>Lorem Ipsum...</p>
<h1>Ble</h1>
<h2>Test</h2>
<h3>Third</h3>
<p>Lorem Ipsum...</p>

This list should be created:

<ul>
    <li>
        <a>Wow</a>
        <ul>
            <li>
                <a>Blablablub</a>
            </li>
        </ul>
    </li>
    <li>
        <a>Lalalala</a>
    </li>
    <li>
        <a>Ble</a>
        <ul>
            <li>
                <a>Test</a>
                <ul>
                    <li>
                        <a>Third</a>
                    </li>
                </ul>
            </li>
        </ul>
    </li>
</ul>

a tags should have a custom id but that isn't important for this question. I tried to do this but I couldn't figure it out. Here's what I tried:

function find_titles(find_num, element, prefix=""){
        temp_list = $("<ul></ul>");
        element.find(`${prefix}h${find_num}`).each(function(i, object){
            let text = $(object).text();
            let id = text.replace(/[^0-9a-zA-Z]/gi, "") + random_chars();

            $(object).attr("id", id);

            if ($(object).next().prop("tagName").toLowerCase() == `h${find_num + 1}`){
                console.log($(object));
                next_titles = find_titles(find_num + 1, $(object), "+ ")[0].innerHTML;
            } else {
                next_titles = "";
            }

            $(`<li><a href="#${id}">${text}</a>${next_titles}</li>`).appendTo(temp_list);
        });
        return temp_list;
    }

EDIT

This:

<h1>First</h1>
<h2>Second</h2>
<p>Lorem Ipsum</p>
<h3>Third</h3>

Should be normally converted into this:

<ul>
    <li>
        <a>First</a>
        <ul>
            <li>
                <a>Second</a>
            </li>
        </ul>
    </li>
    <li>
        <a>Third</a>
    </li>
</ul>

I don't care wether the first is a h1 h2 or a h3. In the text it's only important for styling but in the tree it isn't important.

3 Answers

You can first clear your data to get only heading nodes and their number and text. After that you can loop the data and build tree structure based on levels using array and index number for each level.

function tree(data) {
  data = Array.from(data).reduce((r, e) => {
    const number = e.nodeName.match(/\d+?/g);
    if(number) r.push({ text: e.textContent, level: +number })
    return r;
  }, [])

  const result = $("<ul>")
  const levels = [
    [], result
  ]

  data.forEach(({ level, text }) => {
    const li = $("<li>")
    const a = $("<a>", { text, href: text })
    levels[level + 1] = $('<ul>')

    li.append(a)
    li.append(levels[level + 1]);

    levels[level].append(li)
  })

  return result;
}

const result = tree($("body > *"));
$("body").html(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h1>Wow</h1>
<h2>Blablablub</h2>
<p>Lorem Ipsum...</p>
<h1>Lalalala</h1>
<p>Lorem Ipsum...</p>
<h1>Ble</h1>
<h2>Test</h2>
<h3>Third</h3>
<p>Lorem Ipsum...</p>

You could also do this in one reduce method and add to tree if the element is heading.

function tree(data) {
  const result = $("<ul>")
  const levels = [
    [], result
  ]

  Array.from(data).reduce((r, { textContent: text, nodeName }) => {
    const number = nodeName.match(/\d+?/g);
    const level = number ? +number : null;

    if(level) {
      const li = $('<li>').append($("<a>", { text, href: text }))
      r.push({ level: r[level + 1] = $('<ul>') })
      r[level].append(li.append(levels[level + 1]))
    }

    return r;
  }, levels)

  return result;
}

const result = tree($("body > *"));
$("body").html(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h1>Wow</h1>
<h2>Blablablub</h2>
<p>Lorem Ipsum...</p>
<h1>Lalalala</h1>
<p>Lorem Ipsum...</p>
<h1>Ble</h1>
<h2>Test</h2>
<h3>Third</h3>
<p>Lorem Ipsum...</p>

You can iterate through all the H1 elements and then iterate through all the next header elements (all except H1). Here is an example:

const elements = $('h1').map(function() {
  let container = $('<li>');
  const ret = container;

  container.append($('<a>').text($(this).text()));

  let next = $(this).next('h2, h3, h4, h5');

  while (next.length) {
    const tmp = $('<li>');
    tmp.append($('<a>').text(next.text()));
    container.append(tmp);
    container = tmp;
    next = next.next('h2, h3, h4, h5');
  }
  return ret;
}).get();

const parent = $('<ul>');
parent.append(elements);

console.log(parent[0].innerHTML);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h1>Wow</h1>
<h2>Blablablub</h2>
<p>Lorem Ipsum...</p>
<h1>Lalalala</h1>
<p>Lorem Ipsum...</p>
<h1>Ble</h1>
<h2>Test</h2>
<h3>Third</h3>
<p>Lorem Ipsum...</p>

Using :header selector and tagName property

let $sub, $ul = $('<ul/>')

$(':header').each(function() {
  let $this = $(this),
      $prev = $this.prev(':header'),
      $parent = $prev.length && $prev.prop('tagName') < $this.prop('tagName') ? $sub : $ul
      
  $parent.append('<li><a>' + $this.text() + '</a></li>')
  $sub = $('<ul/>').appendTo($parent.find('li:last'))
})

$('body').html($ul)
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<h1>Wow</h1>
<h2>Blablablub</h2>
<p>Lorem Ipsum...</p>
<h1>Lalalala</h1>
<p>Lorem Ipsum...</p>
<h1>Ble</h1>
<h2>Test</h2>
<h3>Third</h3>
<h3>Third</h3>
<p>Lorem Ipsum...</p>
<h1>First</h1>
<h2>Second</h2>
<p>Lorem Ipsum</p>
<h3>Third</h3>

<h1>First</h1>
<h2>Second</h2>
<p>Lorem Ipsum</p>
<h3>Third</h3>

Related