nexu-io/open-design · error · Error

nested data-od-repeat is not supported

Error message

nested data-od-repeat is not supported

What it means

Thrown by renderFragment() after it strips the repeat element's own directive and then re-scans the remaining body with findRepeatDirective(). If a real (structurally-positioned) directive still exists in the body, the template is rejected because html_template_v1 supports only one level of repetition. Literal `data-od-repeat` text inside comments, quoted attribute values, or prose stays inert and does NOT trigger this — only a genuine second directive element does.

Source

Thrown at apps/daemon/src/live-artifacts/render.ts:253

    const { openTagStart, tagName } = directive;

    const spec = REPEAT_DIRECTIVE_SPEC.exec(directive.spec);
    if (!spec?.[1] || !spec[2]) throw new Error(`invalid data-od-repeat directive: "${directive.spec}" (expected "item in data.path")`);
    const varName = spec[1];
    const arrayPath = spec[2];

    const openTagEnd = findTagEnd(html, openTagStart);
    const selfClosed = html[openTagEnd - 1] === '/';
    const elementEnd = selfClosed ? openTagEnd + 1 : findElementEnd(html, tagName, openTagEnd + 1);
    const element = html.slice(openTagStart, elementEnd);

    // Strip only this element's own directive from its opening tag; a REAL
    // directive anywhere in what remains means a nested repeat, which the
    // contract does not support. Literal mentions inside the repeated body
    // stay inert — same rule as the top-level scan.
    const itemTemplate = element.replace(REPEAT_DIRECTIVE, '');
    if (findRepeatDirective(itemTemplate)) {
      throw new Error('nested data-od-repeat is not supported');
    }

    out += interpolateScalars(html.slice(cursor, openTagStart), resolve);
    for (const item of readArray(arrayPath)) {
      out += renderFragment(itemTemplate, childResolver(resolve, varName, item), readArray);
    }
    cursor = elementEnd;
  }
  return out;
}

export function renderHtmlTemplateV1(input: LiveArtifactRenderInput): LiveArtifactRenderOutput {
  validateHtmlTemplateV1Security(input.templateHtml);

  if (RAW_TEMPLATE_INTERPOLATION.test(input.templateHtml)) {
    throw new Error('raw template interpolation is not supported');
  }

View on GitHub (pinned to 5be4028344)

Solutions

  1. Flatten the data in data.json so a single repeat suffices (e.g. emit a flat list of cells each carrying its row context), then loop once.
  2. Pre-render the nested structure server-side / in the agent before writing data.json, and emit the resulting HTML as a non-template artifact.
  3. Split into sibling repeats at the top level instead of nesting (one repeat per row, each row's cells rendered via scalar bindings if count is fixed).
  4. Do not attempt to work around it by encoding the inner directive as text — the scanner specifically distinguishes real directives from literal text, so a real nested directive will always be caught.

Example fix

// before (unsupported)
<ul data-od-repeat="row in data.rows">
  <li data-od-repeat="cell in row.cells">{{cell.label}}</li>
</ul>
// after — flatten in data.json to data.cells, loop once
<ul data-od-repeat="cell in data.cells">
  <li>{{cell.label}} ({{cell.rowTitle}})</li>
</ul>
Defensive patterns

Strategy: validation

Validate before calling

// Reject templates whose body contains a REAL (structurally positioned) nested directive.
// Reuse the renderer's own scanner to avoid false positives from literal text.
import { renderFragment } from './render'; // findRepeatDirective is not exported; mirror its logic or unit-test through renderFragment

function assertNoNestedRepeat(templateHtml: string): void {
  const re = /\s*\bdata-od-repeat\s*=\s*"([^"]*)"/gi;
  let m: RegExpExecArray | null;
  let count = 0;
  while ((m = re.exec(templateHtml))) {
    const openIdx = templateHtml.lastIndexOf('<', m.index);
    const afterOpen = templateHtml.slice(openIdx).split('>', 1)[0];
    if (!afterOpen.includes('>')) count++;
  }
  if (count > 1) throw new Error('template nests data-od-repeat — flatten the data instead');
}

Prevention

When it happens

Trigger: A repeat element nested inside another repeat element: `<ul data-od-repeat="row in data.rows"><li data-od-repeat="cell in row.cells">...</li></ul>`. Note: the inner directive must be a real element open-tag; `data-od-repeat` text inside a comment or inside another attribute's quoted value is correctly ignored.

Common situations: Rendering tabular data (rows of cells), category→item hierarchies, or any tree where the natural template nests two loops. The html_template_v1 contract is single-pass and explicitly disallows this to keep data-supplied text inert.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/195e600ab20e1ba5. Report an issue: GitHub.