Turning <ul><li>a</li><li>b</li></ul> into a tree of nodes is what DOMParser, template compilers (Vue, Svelte), sanitizers, and syntax highlighters all do under the hood. It's a two-stage classic: a tokenizer scans the string into a flat list of tags and text, and a parser folds that list into a nested tree using a stack. The wrinkles that make it real are attributes, void elements like <img> that never close, and correct nesting.
Implement htmlTokenizerParser(html) returning an array of top-level nodes. See MDN: DOMParser.
function htmlTokenizerParser(html) {
// returns an array of nodes:
// { type: 'element', tag, attributes: {}, children: [] }
// { type: 'text', value }
}
htmlTokenizerParser('<p class="lead">Hi <b>there</b></p>');
// [{ type:'element', tag:'p', attributes:{ class:'lead' }, children:[
// { type:'text', value:'Hi ' },
// { type:'element', tag:'b', attributes:{}, children:[{ type:'text', value:'there' }] },
// ] }]
htmlTokenizerParser('<div><img src="a.png"><span>x</span></div>');
// img is a void element: no children, and the span is its SIBLING, not its child
name="v", name='v', name=v, and bare name (boolean → ''). Lowercase tag names.img, br, input, hr, meta, link, … take no close tag and hold no children; <br/> works too. Don't push them on the stack.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.