nexu-io/open-design · error · Error
unbalanced data-od-repeat element <${tagName}>
Error message
unbalanced data-od-repeat element <${tagName}> What it means
Thrown by findElementEnd() when it cannot match the closing tag for a data-od-repeat element. The function tracks open/close depth of same-named tags (skipping comments) starting just after the opening tag; if depth never returns to zero before the string ends, the repeat element is considered unbalanced. The renderer needs an exact element boundary to slice the repeated body, so a missing or mismatched closing tag is fatal.
Source
Thrown at apps/daemon/src/live-artifacts/render.ts:156
re.lastIndex = openTagEnd;
let depth = 1;
let m: RegExpExecArray | null;
while ((m = re.exec(html))) {
const comment = insideComment(html, m.index);
if (comment) {
re.lastIndex = comment.resumeAt;
continue;
}
const tagEnd = findTagEnd(html, m.index);
if (m[1] === '/') {
depth -= 1;
if (depth === 0) return tagEnd + 1;
} else if (html[tagEnd - 1] !== '/') {
depth += 1;
}
re.lastIndex = tagEnd + 1;
}
throw new Error(`unbalanced data-od-repeat element <${tagName}>`);
}
/** Reads a `data-od-repeat` source path to its array, always from the data root. */
type ArrayReader = (arrayPath: string) => unknown[];
/**
* Render one template fragment against a binding scope, expanding
* `data-od-repeat` elements left to right. Each repeat element is emitted
* fully rendered (all its bindings resolved) so substituted data values are
* never re-scanned by a later interpolation pass — a single-pass invariant
* that keeps data-supplied `{{...}}`-looking text inert. One level only:
* a `data-od-repeat` nested inside another is rejected, matching the
* documented html_template_v1 contract.
*/
/**
* Locate the next REAL `data-od-repeat` directive at or after `from`. A match
* is a directive only when it sits inside an element's open tag: the nearest
* `<` to its left starts a named tag that has not closed yet. LiteralView on GitHub (pinned to 5be4028344)
Solutions
- Locate the data-od-repeat element for the reported tagName and add/fix its matching </tagName> closing tag.
- Ensure the closing tag name exactly matches the opening tag name (spelling identical; dashes included).
- If the body intentionally contains same-named children, confirm each child is itself self-closed or properly closed.
- Wrap the body in a different tag name if you need repeat-over-repeat semantics (nested repeats are unsupported — see error 343).
Example fix
// before
<tr data-od-repeat="row in data.rows">
<td>{{row.a}}</td>
</div>
// after
<tr data-od-repeat="row in data.rows">
<td>{{row.a}}</td>
</tr> Defensive patterns
Strategy: validation
Validate before calling
// Lightweight depth check: for each repeat tagName, ensure open/close counts match (ignoring self-closing).
import { REPEAT_DIRECTIVE } from './render'; // or inline the regex
function assertRepeatElementsBalanced(html: string): void {
const re = /\s*\bdata-od-repeat\s*=\s*"[^"]*"/gi;
let m: RegExpExecArray | null;
while ((m = re.exec(html))) {
const openIdx = html.lastIndexOf('<', m.index);
const name = /^<([A-Za-z][A-Za-z0-9_-]*)/.exec(html.slice(openIdx))?.[1];
if (!name) continue;
const open = (html.match(new RegExp(`<${name}\\b`, 'gi')) || []).length;
const close = (html.match(new RegExp(`</${name}\\s*>`, 'gi')) || []).length;
if (open !== close) throw new Error(`unbalanced repeat element <${name}>: ${open} open vs ${close} close`);
}
} Prevention
- Always pair a data-od-repeat opening tag with a matching </tagName> of the exact same name.
- Validate tag balance on the repeat element specifically before persisting the template.
- Avoid nesting same-named elements inside a repeat unless each is properly closed.
When it happens
Trigger: A repeat element whose closing tag is missing: `<li data-od-repeat="item in data.items">{{item.label}}</ul>` (closed with </ul> instead of </li>). Also when the closing tag name has different casing/spelling, or when a nested same-named element is itself unclosed and consumes the outer closer.
Common situations: Agent emits a repeat over <li> or <tr> but closes the wrong parent; refactoring a template and forgetting to rename the closing tag; copy-paste that drops a closing tag; mismatched casing like `<TR ...>` closed by `</tr>` is tolerated (case-insensitive regex) but a renamed tag like `<row>` closed by `</tr>` is not.
Related errors
- unterminated tag in live artifact template
- invalid data-od-repeat directive: "${directive.spec}" (expec
- data-od-repeat source is not an array: ${arrayPath}
- script elements are not supported in live artifact previews
- iframe elements are not supported in live artifact previews
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/4312c6cebc3631fa.
Report an issue: GitHub.