Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/BaselAshraf81/holystitch/llms.txt

Use this file to discover all available pages before exploring further.

HolyStitch does not use AI to decide what is a component. Instead, it reads the <!-- ComponentName --> markers that Google Stitch embeds in every exported HTML file and uses them as the authoritative component boundaries.

How Stitch marks components

When Stitch exports a screen to HTML, it inserts an HTML comment immediately before each component’s root element:
<body>
  <!-- TopNavBar -->
  <nav class="flex items-center justify-between px-8 py-4 bg-white shadow">
    <img src="logo.svg" data-alt="Logo" />
    <ul class="flex gap-6">
      <li><a href="#">Features</a></li>
      <li><a href="#">Pricing</a></li>
    </ul>
  </nav>

  <!-- HeroSection -->
  <section class="flex flex-col items-center py-24 bg-indigo-50">
    <!-- HeroCta -->
    <div class="flex gap-4 mt-8">
      <button class="btn-primary">Get started</button>
      <button class="btn-ghost">Learn more</button>
    </div>
  </section>

  <!-- Footer -->
  <footer class="bg-gray-900 text-white py-12 px-8">
    <p>&copy; 2024 Acme Inc.</p>
  </footer>
</body>
The comment text becomes the component name after normalization. The block element immediately following the comment becomes that component’s HTML.

Label normalization

Raw comment text is normalized to a valid PascalCase React component name before use. Rules applied in order:
  1. Strip a leading / (for closing comments like <!-- /ComponentName -->)
  2. Strip connector suffixes: from ..., for ..., by ..., - ..., – ...
  3. Strip everything from : onwards — "Entry 1: Latest" becomes "Entry 1"
  4. Convert to PascalCase
  5. Reject if fewer than 3 characters or does not start with an uppercase letter
Raw comment textNormalized name
TopNavBarTopNavBar
TopNavBar from JSON mappingTopNavBar
Entry 1: LatestEntry1
Bold Footer CTABoldFooterCta
<!-- /HeroSection -->HeroSection
ab(rejected — too short)
<!-- a list -->(rejected — starts lowercase)
Only comments that start with an uppercase letter (after stripping a leading /) are treated as component markers. Lowercase comments like <!-- TODO --> or <!-- section start --> are ignored.

The collectAll recursive algorithm

Component extraction is AST-based, not regex-based. HolyStitch parses the body HTML with htmlparser2 (with character position tracking enabled) and then runs collectAll over the resulting node tree.
function collectAll(
  nodes: ChildNode[],
  bodyHtml: string,
  componentDepth: number,
  parentName: string | null
): RawExtraction[] {
  const results: RawExtraction[] = [];

  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]!;

    if (isUppercaseComment(node)) {
      const label = normalizeLabel((node as Comment).data ?? "");
      if (!label) continue;

      // Find next sibling block element (skip whitespace text nodes)
      let j = i + 1;
      while (j < nodes.length &&
        (nodes[j]!.type === "text" || nodes[j]!.type === "comment")) j++;
      const nextEl = j < nodes.length ? nodes[j] : undefined;

      if (nextEl && isBlockElement(nextEl) && meetsContentThreshold(nextEl, bodyHtml)) {
        results.push({ name: label, ...positions, depth: componentDepth, parentName });

        // Recurse INTO the claimed element to find nested sub-components
        const children = (nextEl as Element).children ?? [];
        const nested = collectAll(children, bodyHtml, componentDepth + 1, label);
        results.push(...nested);

        i = j; // skip past the claimed element
        continue;
      }
    }

    // Recurse into non-claimed structural wrappers like <main>, <section>
    if (node.type === "tag") {
      const children = (node as Element).children ?? [];
      if (children.length > 0) {
        const nested = collectAll(children, bodyHtml, componentDepth, parentName);
        results.push(...nested);
      }
    }
  }

  return results;
}
The algorithm visits every node in the tree. When it finds an uppercase comment, it looks for the next sibling block element and “claims” it as a named component. It then recurses inside that element to find any nested components — giving you a full component tree, not a flat list. Structural wrappers that are not preceded by a comment marker (like a <main> or <section> wrapping multiple components) are traversed without being claimed. They remain intact in the page body HTML.

Content threshold

Tiny decorative elements are skipped. An element qualifies as a component only if it meets at least one of:
  • It has at least one child element (not just text)
  • Its outer HTML is at least 300 characters long
function meetsContentThreshold(el: Element, bodyHtml: string): boolean {
  const outerHtmlLength = (el.endIndex ?? 0) - (el.startIndex ?? 0) + 1;
  const hasElementChild = (el.children ?? []).some((c) => c.type === "tag");
  return hasElementChild || outerHtmlLength >= 300;
}

Block elements recognized as component roots

A comment marker can only claim an element whose tag name is in the block element set. Inline elements like <span> or <a> are not claimable.
const BLOCK_ELEMENTS = new Set([
  "div", "section", "article", "main", "header", "footer", "nav", "aside",
  "ul", "ol", "table", "form", "figure", "fieldset", "details", "dialog",
  "canvas", "p", "pre", "h1", "h2", "h3", "h4", "h5", "h6", "li",
]);

Component tree vs flat list

collectAll produces a flat RawExtraction[] array, but each entry carries depth and parentName fields that encode the tree structure. After deduplication, this is converted into ParsedComponent objects:
interface ParsedComponent {
  name: string;
  html: string;      // outer HTML with direct child components replaced by <ChildName />
  rawHtml: string;   // outer HTML, unmodified
  depth: number;     // 0 = direct child of page body
  children: string[]; // direct child component names
}
Components at depth: 0 with parentName: null are root components — they appear directly in the page file’s return statement. Components at deeper depths are sub-components imported and used by their parent. Example tree for the HTML above:
TopNavBar      (depth: 0, parent: null)
HeroSection    (depth: 0, parent: null)
  HeroCta      (depth: 1, parent: HeroSection)
Footer         (depth: 0, parent: null)
This produces four component files. HeroSection.tsx imports and renders <HeroCta />. The page file imports TopNavBar, HeroSection, and Footer.

Page body reconstruction

After extracting all root components, buildPageBody reconstructs the page body HTML with root component blocks replaced by <ComponentName /> self-closing tags:
function buildPageBody(bodyHtml: string, roots: RawExtraction[]): string {
  const sorted = [...roots].sort((a, b) => b.commentStart - a.commentStart);
  let result = bodyHtml;
  for (const ex of sorted) {
    result =
      result.slice(0, ex.commentStart) +
      `<${ex.name} />` +
      result.slice(ex.elementEnd + 1);
  }
  return result;
}
Replacements are applied end-to-start (sorted by commentStart descending) so that earlier character positions remain valid as the string is modified. The result for the HTML example above would be:
<TopNavBar />
<HeroSection />
<Footer />
Structural wrappers that were not claimed by any comment (a surrounding <main>, for example) are preserved intact. This means the page file keeps the original layout intent from Stitch.

Composed component HTML

Each component’s html field is built by replacing its direct child component spans with <ChildName /> placeholders, using the same end-to-start replacement strategy. This gives each component a clean JSX composition body:
// HeroSection.html after buildComposedHtml:
<section class="flex flex-col items-center py-24 bg-indigo-50">
  <HeroCta />
</section>
// HeroCta.html (no children to replace):
<div class="flex gap-4 mt-8">
  <button class="btn-primary">Get started</button>
  <button class="btn-ghost">Learn more</button>
</div>

Cross-screen shared component detection

When the same component name appears on multiple screens, HolyStitch computes Jaccard similarity over word tokens from both HTML strings to determine whether they are the same widget:
// stitch-converter.ts
const SHARED_COMPONENT_SIMILARITY_THRESHOLD = 0.7;
  • ≥ 0.7 similarity — same component; the first screen’s file is reused, subsequent screens skip writing
  • < 0.7 similarity — different widget that happens to share a name; a screen-scoped copy is created as ComponentName + PascalCasedScreenName (e.g., FooterPricingPage), and a warning is written to project-context.md
The sharedComponents field in ConvertStitchOutput lists every component name that appears on two or more screens (above the similarity threshold).
Shared components like TopNavBar and Footer are written once and imported by every page that uses them. To update the navigation across your entire app, you only need to edit one file.

Build docs developers (and LLMs) love