HTML parser written in JavaScript that takes in strings and output a DOM tree object
General review points

Use semicolons
Try to avoid Array iterator functions in favor of for of. Generally use array iterators when you want the iteration to be an expression.

Apart from that the code looks good and is an elegant solution to parsing simple markup. It can be used as a base to provide the additional functionality you mention.
Questions

&quot;Right now the HTML string it takes doesn't have any indentation. How can I modify the code so it can allow indentations in the input strings?&quot;

After you split the markup string you can remove white spaces using String.trim
In the example parser (bottom of answer), the first expression uses Array.map and Array.filter to trim white spaces and then remove empty lines.
If you want to only remove white spaces using the same rules as HTML then you can use a RegExp and  String.replace to remove white spaces.
Token validator

&quot;How can I modify the code so it can become easily expandable for other HTML tag body, div, span etc.&quot;


&quot;Right now the HTML string it takes has to be valid. How can I modify the code so it can detect invalid HTML&quot;

For the other questions you will need to create an object that can validate tokens. The validators role is to recognize, valid tags, invalid tags, and content.
It does this

by keeping a Set of tag names
providing a function to check a token. If the token is a valid closing tag it returns the token representing the opening tag.
keeps the state of the token tested. If the token is unknown or a tag the semaphore are set appropriately.

Your function has been modified to use the validator and check for various problems related to invalid markup.
The function will return a simple error object containing the error description if the markup is invalid
If the token validator checks a token that is unknown (not in name list and starts or ends with &lt; or &gt;) unknown is set true.
If the token validator checks a known opening tag the semaphore tag is set true.
The semaphore are reset on each token checked.
The Validator
Add the tags you wish to recognize to the names Set.
Call tokenValidator.token to validate a token.
It will return the opening tag as a token or undefined.
If undefined the check the semaphores  tokenValidator.tag and / or tokenValidator.unknown to determine what to do with the current token.
const tokenValidator = (() =&gt; {
    const names = new Set([&quot;div&quot;, &quot;span&quot;, &quot;p&quot;]);
    var unknown = false, tag = false;
    return {
        get unknown() { return unknown },
        get tag() { return tag },
        token(token) {
            tag = unknown = false;
            const hasEnd = token.endsWith(&quot;&gt;&quot;);
            if (token.startsWith(&quot;&lt;&quot;)) {
                if (token.startsWith(&quot;&lt;/&quot;) &amp;&amp; hasEnd) {
                    const tokenName = token.slice(2,-1);
                    if (names.has(tokenName)) { return &quot;&lt;&quot; + tokenName + &quot;&gt;&quot; }
                    tag = unknown = true;
                } else if (hasEnd) {
                    const tokenName = token.slice(1,-1);
                    unknown = !names.has(tokenName);
                    tag = true;
                    return;
                }
                unknown = true;
            } else if (hasEnd) { tag = unknown = true }
        },
    };
})();

The Parser
The parses is based on your original code.
It will either return the parsed markup or an error.
Usage example
const result = parser(HTML);
if (result.error) { console.log(result.message) }
else { console.log(result) }

To check for hanging tags (eg missing closing tags) the function counts up on opening tags and down on closing tag. The countOpen must be 0 when done or there is an error.
Example Code
function parser(html) {
    const tokens = html.split('\n').map(token =&gt; token.trim()).filter(token =&gt; token !== &quot;&quot;);
    const error = message =&gt; ({error: true, message: &quot;Invalid HTML. &quot; + message});
    const stack = [];
    var countOpen = 0;
    for (const token of tokens) {
        const openToken = tokenValidator.token(token);
        if (tokenValidator.unknown) { return error(&quot;Unknown tag: &quot; + token) }
        if (openToken) {
            const children = [];
            let stackToken = stack.pop();
            while (stackToken?.value !== openToken) {
                if (!stackToken) { return error(&quot;Tag order: &quot; + token) }
                children.unshift(stackToken);
                stackToken = stack.pop();
            }
            stack.push({value: openToken.slice(1, -1), children});
            countOpen--;
        } else if (tokenValidator.tag) {
            stack.push({value: token});
            countOpen ++;
        } else { stack.push(token) }
    }
    return countOpen ? error(&quot;Missing tag&quot;) : stack.pop();
}

Note that the code does not cover all invalid markup. It is just an example intended to demonstrate how validation can be addressed.