8

The recursion part is quite illusive.
for a given HTML structure, of unknown depth, I need to convert to JSON.
(I use this from some YAML internationalization translation system I am building)

my general idea is to go deep until it finds the INPUT, then create an object with the key/value of the span.innerHTML/input. Value, and return that object, so it will be the VALUE of a KEY that is the last <span class="title"> reached.


JSBIN playground - live code example

I can't get my recursive function to output the JSON I want...

HTML structure

<ul>
    <li>
        <span class="title">footer</span>
        <ul>
            <li>
                <span>statement</span>
                <input type="text" value="xxx">
            </li>
        </ul>
    </li>
    <li>
        <span class="title">landing</span>
        <ul>
            <li>
                <span>page_title</span>
                <input type="text" value="yyy">
            </li>
            <li>
                <span>page_sub_title</span>
                <input type="text" value="xxx">
            </li>
            <li>
                <span class="title">pricing</span>
            <ul class="level11">
                <li>
                    <span>title</span>
                    <input type="text" value="aaa">
                </li>
                <li>
                    <span>cost</span>
                    <input type="text" value="xxx">
                </li>
            </ul>
            </li>
        </ul>
    </li>
</ul>

Wanted JSON output

{
    footer : {
        statement : 'xxx'
    },
    landing : {
        page_title : 'yyy',
        page_sub_title : 'xxx',
        pricing : {
            title : 'aaa',
            cost : 'xxx'
        }
    }
}
MatthewMartin
  • 32,326
  • 33
  • 105
  • 164
vsync
  • 118,978
  • 58
  • 307
  • 400
  • Is the absence of a helping library (e.g. jQuery) intended? Similarly the usage of [document.querySelectorAll](https://developer.mozilla.org/En/DOM/Document.querySelectorAll)? I mean, it would make things so much easier... – Yoshi Nov 03 '11 at 10:11
  • Just a quick sidenote: have you heard of `label`? You should use it. – kapa Nov 03 '11 at 10:16

5 Answers5

9

I'm new here and i couldn't find how to post a comment. I wanted to ask you if this is always the structure, no matter the dept. If the answer is no, then don't read my answer :).

So first of all i added a function getPrevious, because directly trying to get the previous sibling returns you a text node. Next i changed the recursion a little bit, because it's not a simple recursion, the json format (the parent-child relations) is different then the html format. I tried it for 2 more levels and it's ok. I hope it's helpful and sorry if it's not.

    function getPrevious(element)
    {
        var prev_el = element.previousSibling;
        while (prev_el.nodeType == 3)
        {
            prev_el = prev_el.previousSibling;
        }
        return prev_el;
    }

    function recursive(element){
        //var classname = element.className.split(' ');
        // element.nodeName == 'UL'
        var Result = {"title": '', "json": {}};
        var json = {};
        var cur_json_key = '';
        if( element.nodeType == 3 )
            return;
        else{
            //console.log( element.nodeType, element );

            var nodeName = element.nodeName.toLowerCase();
            var nodeClass = element.className.toLowerCase();

            // if this is the SPAN with class 'TITLE', then create an object with the innerHTML as KEY
            // and later the value should be another object, returned from the recursion...
            if( nodeName == 'span' && nodeClass == 'title' ){
                json[element.innerHTML] = {};
                Result.title = element.innerHTML;
                Result.json = json;
            }
            else
            if( nodeName == 'input' ){
                // if this is an INPUT field, then the SPAN sibling before it is the KEY.
                var key = getPrevious(element).innerHTML;
                var val = element.value;
                Result.json[key] = val;
            }
            else
            {
                var is_title_found = 0;
                var title_found = '';
                var res = {}
                // go deeper
                for( var child=0; child < element.childNodes.length; child++ ){
                    //json = $.extend( {}, recursive( element.childNodes[child] ));
                    res = recursive( element.childNodes[child]);
                    if (res)
                    {
                        if (res.title != '')
                        {
                            is_title_found = 1;
                            title_found = res.title;
                        }
                        else
                        {
                            $.extend(true, json, res.json);
                        }
                        console.log(JSON.stringify(json));
                    }
                }
                if (title_found)
                {
                    Result.json[title_found] = json
                }
                else
                {
                    Result.json = json;
                }
            }
            return Result;
        }
    }
fortune
  • 318
  • 1
  • 6
  • yes this is always the structure no matter the depth. thanks for posting an answer! – vsync Nov 03 '11 at 12:01
  • 1
    @fortune : adding comments can only be done with enough reputation, but you won't have to wait long to get that rep. Here's your first 10 extra ;) – Joris Meys Nov 03 '11 at 12:58
5

If you can convince yourself to using jQuery, try this:

function helper(root) {
  var result = {};

  $('> ul > li > span', root).each(function () {
    result[$(this).text()] = $(this).hasClass('title') ? helper($(this).parent()) : $(this).next('input').val();
  });

  return result;
}

console.log(helper('body'));
Yoshi
  • 54,081
  • 14
  • 89
  • 103
2
<section id="in">
    <ul>
        <li><div>lorem</div></li>
        <li>
            <div>lorem</div>
            <ul>
                <li><div>lorem</div></li>
                <li>
                    <div>lorem</div>
                </li>
                <li>
                    <div>lorem</div>
                    <ul>
                        <li><div>lorem</div></li>
                        <li>
                            <div>lorem</div>
                        </li>
                        <li><div>lorem</div></li>
                        <li><div>lorem</div></li>
                    </ul>
                </li>
                <li><div>lorem</div></li>
            </ul>
        </li>
        <li><div>lorem</div></li>
        <li><div>lorem</div></li>
    </ul>
</section>

<textarea id="outjson"></textarea>

    var a = [];
    getJSON($('#in'), a);
    function getJSON(el, arr)
    {
        el.children().each(function()
        {
            arr.push({});
            arr[arr.length-1][this.tagName] = [];
            if ($(this).children().length > 0)
            {
                getJSON($(this), arr[arr.length-1][this.tagName]);
            }
        });
    }
    $('#outjson').text(JSON.stringify(a));

You will get:

[{"UL":[{"LI":[{"DIV":[]}]},{"LI":[{"DIV":[]},{"UL":[{"LI":[{"DIV":[]}]},{"LI":[{"DIV":[]}]},{"LI":[{"DIV":[]},{"UL":[{"LI":[{"DIV":[]}]},{"LI":[{"DIV":[]}]},{"LI":[{"DIV":[]}]},{"LI":[{"DIV":[]}]}]}]},{"LI":[{"DIV":[]}]}]}]},{"LI":[{"DIV":[]}]},{"LI":[{"DIV":[]}]}]}]

1

Try this:

function helper(root) {
  var result = {};

  root.querySelectorAll(':scope > ul > li > span').forEach(function (obj) {
      result[obj.innerText] = obj.classList.contains('title') ? helper(obj.parentNode) : obj.parentNode.querySelector('input').value;
  });

  return result;
}

console.log(helper(document.querySelector('body')));
Marcelo Matos
  • 11
  • 1
  • 5
0

Live Example

var ul = document.body.firstElementChild;
// cheat to only extract the value (key is undefined)
var data = extractKeyValue({}, ul)[1];


function extractKeyValue(span, thing) {
  // return key & input value
  if (thing.tagName === "INPUT") {
      return [span.textContent, thing.value];
  } else {
    // recurse over every li and return the key/value of the span + thing
    var obj = {};
    [].forEach.call(thing.children, function (li) {
      var span = li.firstElementChild;
      var thing = span.nextElementSibling;
      // tuple is [key, value]
      var tuple = extractKeyValue(span, thing);
      obj[tuple[0]] = tuple[1];
    });
    return [span.textContent, obj];
  }
}
Raynos
  • 166,823
  • 56
  • 351
  • 396