Skip to content

Improve DX around Invalid HTML position (placeholder count mismatch after HTML parse) #125

Description

@amitchaudharyc

Summary

When using Arrow for agent-generated UIs, we frequently hit:

Error: Invalid HTML position

thrown from template compilation when the number of expression placeholders recovered from template.innerHTML does not match rawStrings.length - 1.

In @arrow-js/core@1.0.6 this is roughly:

template.innerHTML = signature;
const paths = createPaths(template.content);
// ...
if (count !== expressions) {
  throw Error('Invalid HTML position');
}

This is understandable given HTML parser rules (comment placeholders can be moved/dropped when expressions land in illegal positions). For coding agents and humans alike, the current error is hard to act on: it does not say which expression was lost, where in the template, or why.

Why this matters

Arrow is positioned as especially good for agent-authored interfaces (tiny docs, no build step). Agents commonly emit patterns that are idiomatic in React/Vue/string templates but illegal for Arrow’s placeholder approach, e.g.:

  • mixed static + dynamic attribute text: class="truncate ${cls}"
  • interpolated ids/labels: id="row-${id}", aria-label="Open ${name}"
  • HTML comments inside html\...``
  • whitespace-wrapped table rows: html` <tr>...</tr> `
  • non-<tr> children under <tbody>

We can paper over this with prompt rules, but a clearer runtime signal (or safer compile path) would reduce a large class of agent failures.

Minimal reproductions

1) Mixed attribute interpolation (very common)

<!doctype html>
<html>
  <body>
    <div id="app"></div>
    <script type="module">
      import { html, reactive } from 'https://esm.sh/@arrow-js/core@1.0.6';

      const state = reactive({ selected: true, name: 'Pump' });
      const nameClass = () => (state.selected ? 'font-semibold text-blue-700' : 'font-semibold text-slate-900');

      // Throws: Invalid HTML position
      html`<div class="truncate ${nameClass}">${() => state.name}</div>`(document.getElementById('app'));
    </script>
  </body>
</html>

Works if rewritten to a single full attribute expression:

const assetNameClass = () => `truncate ${nameClass()}`;
html`<div class="${assetNameClass}">${() => state.name}</div>`

2) Interpolated id / aria-label

html`<div id="asset-${() => state.id}"></div>`
// and
html`<button aria-label="Open ${() => state.name}">Open</button>`

Both throw Invalid HTML position. Precomputing the full attribute string works.

3) HTML comment inside template

html`<!-- row --><section>${() => state.name}</section>`

Throws Invalid HTML position (comments collide with Arrow’s placeholder strategy).

4) Related: whitespace around <tr> (also tracked in #124)

html`<tbody>${() => [html` <tr><td>1</td></tr> `]}</tbody>`

Can break rendering / throw depending on updates; related HTML-parser sensitivity.

Expected behavior (DX ask)

Any of these would help a lot for agent + human authors:

  1. Better error message when placeholder count mismatches, e.g.:
    • expected N expressions, found M after HTML parse
    • include a short snippet of the offending template signature
    • list common causes (mixed attributes, comments, illegal table content, etc.)
  2. Docs / llms.txt callout with a short “illegal positions” section and wrong/right examples (mixed attrs especially).
  3. Longer-term (optional): compile-time detection or a more resilient attribute interpolation story so class="foo ${bar}" either works or fails with an attribute-specific diagnostic instead of a generic position error.

Environment

  • @arrow-js/core 1.0.6
  • Browser Chromium (also reproduced under Puppeteer verification)
  • Use case: LLM-generated Arrow apps in a sandboxed iframe

Notes

We understand this may be “working as designed” given innerHTML + comment placeholders. Filing this mainly as a DX / agent-ergonomics request: the failure mode is correct, but the signal is too opaque for the audience Arrow is targeting.

Happy to help refine repros or test patches.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions