"Which kinds of elements does this subtree use?" is a handy audit — for a linter, a sanitizer allowlist check, or just understanding a chunk of markup. The answer is the distinct set of tag names, and you usually want them in the order they first appear (document order), lowercased so <DIV> and <div> count as one. It's a classic "traverse + dedupe while preserving order" problem, applied to the DOM.
Implement getDomTags(root). Return the distinct lowercase tag names in the subtree (including the root), in first-appearance document order. A null root yields [].
function getDomTags(root) {
// returns string[] of distinct lowercase tags, in document order
}
// <div><p></p><span></span><p></p></div>
getDomTags(div); // ['div', 'p', 'span'] (second <p> is a duplicate)
// <section><ARTICLE></ARTICLE></section>
getDomTags(section); // ['section', 'article'] (lowercased)
querySelectorAll('*') already returns).element.tagName is uppercase for HTML; normalize with .toLowerCase().Set for membership plus an array for order, keeping each tag's first position.You'll list the root plus all descendants in document order, lowercase each tag, and collect them into an array while a Set skips duplicates.
Two sub-problems stack. First, visit every element in document order — a preorder walk of the subtree, which [root, ...root.querySelectorAll('*')] gives you for free (querySelectorAll returns matches in document order). Second, dedupe while preserving first-appearance order — the standard "seen set + result array" idiom: for each tag, if you haven't seen it, record it and mark it seen. Lowercasing normalizes HTML's uppercase tagName so casing doesn't create phantom duplicates.
Walk the elements top to bottom, left to right. Keep a Set of tags you've already emitted and an array in the order you emitted them. For each element, lowercase its tagName; if it's new to the Set, push it to the array and add it to the Set. The Set gives O(1) "have I seen this?" while the array remembers when you first saw it — so the output is distinct and ordered.
The tempting one-liner loses order or casing:
function getDomTagsNaive(root) {
const tags = [...root.querySelectorAll('*')].map((el) => el.tagName);
return [...new Set(tags)]; // misses the ROOT, and tags are UPPERCASE
}
Two bugs. querySelectorAll('*') returns descendants only, so the root's own tag is missing. And el.tagName is uppercase for HTML elements, so <DIV> and <div> would be treated as different tags (and the output is shouty). new Set does preserve insertion order, which is nice — but we still need to include the root and lowercase before deduping.
function getDomTags(root) {
if (!root) return [];
// Root first, then all descendants — both in document order.
const elements = [root, ...root.querySelectorAll('*')];
const seen = new Set();
const result = [];
for (const el of elements) {
const tag = el.tagName.toLowerCase();
if (!seen.has(tag)) {
seen.add(tag);
result.push(tag);
}
}
return result;
}
module.exports = { getDomTags };
[root, ...root.querySelectorAll('*')] is every element in the subtree, root included, in document order. We lowercase each tagName and use the classic seen/result pair: the Set answers "already emitted?" in O(1), and the array records the first-appearance order. Text and comment nodes never appear because querySelectorAll matches elements only. The whole thing is O(n) over the elements.
getDomTags on <div><a></a><b></b><a></a><c></c></div>:
[div, a, b, a, c].div — "div" not seen → push → ['div'].a — "a" not seen → push → ['div', 'a'].b — "b" not seen → push → ['div', 'a', 'b'].a (second one) — "a" already in the Set → skip.c — "c" not seen → push → ['div', 'a', 'b', 'c'].The repeated <a> didn't move — the first occurrence fixed its position.
querySelectorAll('*') is descendants only; prepend root.tagName is uppercase for HTML; <DIV>/<div> would double-count without .toLowerCase().[...new Set(...)] alone — works for dedupe+order, but only if you've already handled root and casing; otherwise it faithfully preserves the wrong data.childNodes vs elements — use querySelectorAll/children, not childNodes, so text nodes don't sneak in (they have no tagName).Set for a Map<tag, count> gives a histogram ("how many of each element"), useful for markup profiling.tagName isn't uppercased); a robust version decides whether to normalize per namespace.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
"Which kinds of elements does this subtree use?" is a handy audit — for a linter, a sanitizer allowlist check, or just understanding a chunk of markup. The answer is the distinct set of tag names, and you usually want them in the order they first appear (document order), lowercased so <DIV> and <div> count as one. It's a classic "traverse + dedupe while preserving order" problem, applied to the DOM.
Implement getDomTags(root). Return the distinct lowercase tag names in the subtree (including the root), in first-appearance document order. A null root yields [].
function getDomTags(root) {
// returns string[] of distinct lowercase tags, in document order
}
// <div><p></p><span></span><p></p></div>
getDomTags(div); // ['div', 'p', 'span'] (second <p> is a duplicate)
// <section><ARTICLE></ARTICLE></section>
getDomTags(section); // ['section', 'article'] (lowercased)
querySelectorAll('*') already returns).element.tagName is uppercase for HTML; normalize with .toLowerCase().Set for membership plus an array for order, keeping each tag's first position.You'll list the root plus all descendants in document order, lowercase each tag, and collect them into an array while a Set skips duplicates.
Two sub-problems stack. First, visit every element in document order — a preorder walk of the subtree, which [root, ...root.querySelectorAll('*')] gives you for free (querySelectorAll returns matches in document order). Second, dedupe while preserving first-appearance order — the standard "seen set + result array" idiom: for each tag, if you haven't seen it, record it and mark it seen. Lowercasing normalizes HTML's uppercase tagName so casing doesn't create phantom duplicates.
Walk the elements top to bottom, left to right. Keep a Set of tags you've already emitted and an array in the order you emitted them. For each element, lowercase its tagName; if it's new to the Set, push it to the array and add it to the Set. The Set gives O(1) "have I seen this?" while the array remembers when you first saw it — so the output is distinct and ordered.
The tempting one-liner loses order or casing:
function getDomTagsNaive(root) {
const tags = [...root.querySelectorAll('*')].map((el) => el.tagName);
return [...new Set(tags)]; // misses the ROOT, and tags are UPPERCASE
}
Two bugs. querySelectorAll('*') returns descendants only, so the root's own tag is missing. And el.tagName is uppercase for HTML elements, so <DIV> and <div> would be treated as different tags (and the output is shouty). new Set does preserve insertion order, which is nice — but we still need to include the root and lowercase before deduping.
function getDomTags(root) {
if (!root) return [];
// Root first, then all descendants — both in document order.
const elements = [root, ...root.querySelectorAll('*')];
const seen = new Set();
const result = [];
for (const el of elements) {
const tag = el.tagName.toLowerCase();
if (!seen.has(tag)) {
seen.add(tag);
result.push(tag);
}
}
return result;
}
module.exports = { getDomTags };
[root, ...root.querySelectorAll('*')] is every element in the subtree, root included, in document order. We lowercase each tagName and use the classic seen/result pair: the Set answers "already emitted?" in O(1), and the array records the first-appearance order. Text and comment nodes never appear because querySelectorAll matches elements only. The whole thing is O(n) over the elements.
getDomTags on <div><a></a><b></b><a></a><c></c></div>:
[div, a, b, a, c].div — "div" not seen → push → ['div'].a — "a" not seen → push → ['div', 'a'].b — "b" not seen → push → ['div', 'a', 'b'].a (second one) — "a" already in the Set → skip.c — "c" not seen → push → ['div', 'a', 'b', 'c'].The repeated <a> didn't move — the first occurrence fixed its position.
querySelectorAll('*') is descendants only; prepend root.tagName is uppercase for HTML; <DIV>/<div> would double-count without .toLowerCase().[...new Set(...)] alone — works for dedupe+order, but only if you've already handled root and casing; otherwise it faithfully preserves the wrong data.childNodes vs elements — use querySelectorAll/children, not childNodes, so text nodes don't sneak in (they have no tagName).Set for a Map<tag, count> gives a histogram ("how many of each element"), useful for markup profiling.tagName isn't uppercased); a robust version decides whether to normalize per namespace.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.