npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

viewschema

v0.0.6

Published

Template engine for dynamic HTML with schema-driven data binding

Downloads

23

Readme

ViewSchema

Attribute-driven HTML templates with data binding, rendering, composition, and JSON Schema extraction. Author templates in plain HTML with view-* directives. Render with data. Extract strict contracts. One source, two outputs.

Install: npm i viewschema

Requirements: Node 16+


Quick example

<article view-object="post">
  <h1 view-text="title">Default Title</h1>
  <p view-html="content">Default content</p>
  <a view-attr:href="url" view-text="linkText">Read more</a>
</article>
import { render, extract } from 'viewschema';

const data = {
  post: {
    title: 'Hello World',
    content: '<em>Rich</em> content',
    url: '/posts/1',
    linkText: 'Continue reading'
  }
};

const html = render(template, data);
// <article><h1>Hello World</h1><p><em>Rich</em> content</p>…

const schema = extract(template);
// { type: "object", properties: { post: { … } }, required: […], … }

The schema is strict: all discovered properties are required, additionalProperties: false.


Authoring templates

Write normal HTML. Add view-* attributes to bind data. The renderer validates your template once, then renders fast.

Data paths

Dot notation: user.name, cta.url

Identity binding: "" or "." references the current context

  • Allowed: content bindings (view-text, view-html)
  • Not allowed: attribute bindings (view-attr:*), usage-level prompts

Content bindings

view-text="path"

Sets textContent, HTML-escaped by default.

<h1 view-text="title">Fallback</h1>
  • Objects/arrays: empty → blank; non-empty → JSON-stringified with preserved quotes for readability
  • Identity path allowed: view-text="" or view-text="."
  • Schema: string property with minLength: 1 by default

view-html="path"

Sets innerHTML raw. Use only with trusted content.

<div view-html="richContent">Fallback</div>
  • Same stringification rules as view-text
  • Identity path allowed
  • Schema: same as view-text

view-markdown="path"

Converts markdown content to HTML and sets innerHTML. Useful for AI-generated content.

<article view-markdown="body">Fallback content</article>
  • Markdown is parsed and converted to HTML (headings, paragraphs, bold, italic, code blocks, links, lists, tables, etc.)
  • HTML entities are escaped to prevent XSS
  • Objects/arrays: same stringification rules as view-text
  • Identity path allowed: view-markdown="" or view-markdown="."
  • Schema: string property with minLength: 1 by default
  • Automatic prompt: When no view-prompt is provided, the schema description defaults to "Use markdown formatting."

Supported markdown features:

  • Headings (h1-h6)
  • Paragraphs
  • Bold (**text** or __text__)
  • Italic (*text* or _text_)
  • Strikethrough (~~text~~)
  • Inline code (`code`)
  • Code blocks (```language\ncode\n```)
  • Links ([text](url))
  • Images (![alt](url))
  • Blockquotes (> text)
  • Unordered lists (- item or * item)
  • Ordered lists (1. item)
  • Horizontal rules (---, ***, ___)
  • Tables (| col1 | col2 |)

Rule: view-text, view-html, and view-markdown cannot coexist on the same element.

Attribute bindings

view-attr:[attribute]="path"

Binds any attribute to data.

<a view-attr:href="url" view-text="label">Link</a>
<img view-attr:src="imagePath" view-attr:alt="imageAlt" />
<button view-attr:disabled="isLoading">Submit</button>
  • Boolean attributes (disabled, checked, selected, readonly, required, multiple, autofocus, hidden): present when truthy, removed when falsy
  • Special case: binding src removes any existing srcset to avoid conflicts
  • Identity path not allowed
  • Schema: boolean attributes infer type: boolean; others default to string

Object scope

view-object="path"

Switches data context for the element and all children.

<section view-object="hero">
  <h1 view-text="title">Title</h1>
  <p view-text="subtitle">Subtitle</p>
</section>
  • Render: if the path doesn't resolve to a non-null object, the element is removed
  • Schema: contributes an object property; children populate properties

Loops

<template view-each="arrayPath"> ... </template>

Repeats content for each array item.

<ul>
  <template view-each="items">
    <li view-text="name">Item</li>
  </template>
</ul>
  • The <template> element itself never renders
  • Schema: array property; children determine items schema

Primitive arrays

When the loop body contains an empty/identity content binding with no other bindings:

<template view-each="tags">
  <span view-text=""></span>
</template>

Schema infers items: { type: "string" }.

Array constraints

<template view-each="features" view-items="3">…</template>
<template view-each="options" view-min-items="1" view-max-items="10">…</template>
  • view-items="N": exact count
  • Or view-min-items and/or view-max-items
  • Rules: non-negative integers; choose exact OR min/max; min ≤ max

Implicit array sizes

Indexed bindings infer constraints:

<span view-text="items.0.label">A</span>
<span view-text="items.2.label">C</span>

Schema sets items.minItems: 3, maxItems: 3.

Rules

  • view-each must be on <template>
  • Cannot combine view-each and view-object on the same element

Conditionals

view-if="path"

Removes the element when falsy. Note that view-if is read-only and does NOT create a schema for the path, if needed, use (for example) view-declare="showElement" view-type="boolean" .

<div view-if="showDetails">Details here</div>
  • Evaluated after applying nearest view-object scope
  • Schema: no direct property contribution

Tag transforms

view-tagname="path"

Changes the element's tag at render time.

<h2 view-tagname="level" view-text="heading">Heading</h2>

With enum syntax:

<h2 view-tagname="tag=h1|h2|h3|h4" view-text="heading">Heading</h2>
  • Enum tokens must match [a-z][a-z0-9-]*
  • Schema: string property, with enum if syntax used

Type hints

No numeric inference from defaults. Declare types explicitly.

Content: view-type="string|number|boolean|object|array"

<span view-type="number" view-text="age">0</span>

Attributes: view-type:[attr]="type"

<div view-attr:data-count="count" view-type:data-count="number"></div>

Tag transforms: view-type:tagname="string"

Constraints

ViewSchema supports JSON Schema constraints for strings and numbers. Constraints can be applied to content bindings, attribute bindings, and tagname bindings.

String constraints

Length constraints:

  • view-min-length="n" — minimum string length (non-negative integer)
  • view-max-length="n" — maximum string length (non-negative integer)
  • view-length="n" — target length with variance (computes min/max)
  • view-length-variance="0.15" or "15%" — variance for target length (default: 15%)

Pattern constraint:

  • view-pattern="regex" — JSON Schema regex pattern for validation

Examples:

<!-- Simple min/max -->
<h1 view-text="title" view-min-length="3" view-max-length="120">Title</h1>

<!-- Target length with default 15% variance -->
<p view-text="summary" view-length="100">Summary</p>
<!-- Generates minLength: 85, maxLength: 115 -->

<!-- Custom variance -->
<p view-text="bio" view-length="200" view-length-variance="25%">Bio</p>
<!-- Generates minLength: 150, maxLength: 250 -->

<!-- Pattern validation -->
<input view-attr:value="email" view-pattern="^[^@]+@[^@]+\.[^@]+$" />

Targeted attribute constraints:

Use :attribute suffix to scope constraints to specific attributes:

<a 
  view-attr:href="url"
  view-min-length:href="10"
  view-max-length:href="2048"
  view-pattern:href="^https?://"
>Link</a>

<img
  view-attr:alt="altText"
  view-length:alt="40"
  view-length-variance:alt="25%"
/>

Tagname constraints:

Use :tagname suffix for view-tagname bindings:

<div
  view-tagname="elementType"
  view-min-length:tagname="2"
  view-max-length:tagname="32"
  view-pattern:tagname="^[a-z][a-z0-9-]*$"
>Content</div>

Number constraints

Number constraints require view-type="number" (for content) or view-type:attr="number" (for attributes).

  • view-minimum="n" — minimum value (finite number)
  • view-maximum="n" — maximum value (finite number)
  • view-multiple-of="n" — value must be multiple of this positive number

Examples:

<!-- Content binding -->
<output 
  view-text="score"
  view-type="number"
  view-minimum="0"
  view-maximum="100"
  view-multiple-of="5"
>0</output>

<!-- Attribute binding -->
<input
  view-attr:value="age"
  view-type:value="number"
  view-minimum:value="0"
  view-maximum:value="150"
/>

Schema output:

Constraints are added to the generated JSON Schema:

{
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 120
    },
    "email": {
      "type": "string",
      "pattern": "^[^@]+@[^@]+\\.[^@]+$"
    },
    "score": {
      "type": "number",
      "minimum": 0,
      "maximum": 100,
      "multipleOf": 5
    }
  }
}

Validation:

  • String constraints require string-typed bindings
  • Number constraints require number-typed bindings
  • Targeted constraints require corresponding bindings (view-attr:* or view-tagname)
  • Min cannot exceed max
  • Pattern must be valid regex
  • Variance must be 0-100% or 0-1 fraction
  • multipleOf must be positive

Descriptions

Add view-prompt for schema documentation.

Content bindings:

<h1 view-text="title" view-prompt="Page title">Title</h1>

Attributes:

<a view-attr:href="url" view-prompt:href="Destination URL">Link</a>

Objects:

<div view-object="user" view-prompt="User information">…</div>

Arrays:

<template view-each="items" view-prompt="List of features">…</template>

Root schema:

The first top-level element with view-prompt sets the root schema description.

Schema-only declarations

view-declare="path"

Adds a schema property without rendering. (Useful for a conditional used in view-if)

<div view-declare="internalFlag" view-type="boolean"></div>
  • Defaults to string with minLength: 1
  • Use view-type to override
  • Stripped from rendered output

Enums

Inline enum syntax works anywhere: key=value1|value2|...

<span view-text="status=draft|published|archived">Status</span>
<button view-attr:type="buttonType=button|submit|reset">Action</button>

Schema: { type: "string", enum: [...] }

Testing hooks

view-testid="id"

Preserved in output for test selectors.

<button view-testid="submit-btn" view-text="label">Submit</button>

Composition with fragments

<template view-use="fragmentName"></template>

Injects reusable template fragments from a TemplateMap.

<!-- Usage -->
<main>
  <template view-use="hero"></template>
  <template view-use="footer"></template>
</main>
const templates = {
  hero: `<section view-object="hero">
    <h1 view-text="title">Title</h1>
    <p view-text="subtitle">Subtitle</p>
  </section>`,
  footer: `<footer view-text="copyright">© 2024</footer>`
};

render(html, data, undefined, templates);

Scoping with view-object

<template view-use="card" view-object="featured"></template>

Context rewrite rules:

  • Usage has outer, fragment root has inner → fragment becomes outer.inner
  • Fragment root has no view-object → adopts outer
  • Identity . → adopts current scope at runtime

Combining with loops

<template view-each="cards" view-use="card"></template>

Schema behavior

  • Fragment object scopes emit to $defs keyed by fragment name
  • Usage property becomes { $ref: "#/$defs/<fragmentName>" }
  • Dotted usage context (e.g., page.hero) uses first segment as property; nested structure lives in $defs

Usage-level descriptions

view-prompt on <template view-use>:

  • With view-each: describes the array
  • With concrete view-object: describes the object container
  • Without either: forwarded to unique top-level array/object in fragment (errors if ambiguous)
  • Identity context invalid for prompts

Validation

  • Requires templates map
  • Missing fragments error early
  • Cycles detected and rejected

View stacks

view-stack="allowlist"

Models heterogeneous arrays with discriminators.

<template view-each="blocks" view-stack="widget|note|field">
  <!-- Inner content ignored when view-stack present -->
</template>
const data = {
  blocks: [
    { is: 'widget', name: 'Counter' },
    { is: 'note', text: 'Important!' },
    { is: 'field', label: 'Email' }
  ]
};

Filtering

<!-- All templates -->
<template view-each="blocks" view-stack></template>

<!-- Explicit allowlist -->
<template view-each="blocks" view-stack="widget|note"></template>

<!-- Glob patterns -->
<template view-each="blocks" view-stack="wi*|no?e"></template>

Rendering

For each item, looks up item.is to select the fragment. Errors if is missing or not allowed.

Schema

{
  "items": {
    "oneOf": [
      { "$ref": "#/$defs/widget" },
      { "$ref": "#/$defs/note" }
    ]
  }
}

Each $defs entry:

  • Includes discriminator: is: { const: "<fragmentName>" }
  • Strict object: additionalProperties: false
  • Required: ["is", ...fragmentRequired]

Rules

  • Must be on <template view-each="...">
  • Requires templates map

Processing order

Rendering follows this sequence:

  1. Expand fragments (view-use)
  2. Expand loops (view-each)
  3. Apply object scopes (view-object)
  4. Apply conditionals (view-if)
  5. Transform tags (view-tagname)
  6. Bind content (view-text, view-html)
  7. Bind attributes (view-attr:*)
  8. Strip metadata (view-prompt, view-type, view-declare, internal markers)

Elements removed when:

  • view-if evaluates falsy
  • view-object doesn't resolve to a non-null object

Default content cleared before binding.

Text escaped; view-html raw by design.

Whitespace-only text nodes are preserved as authored (including formatting whitespace between elements).


Schema extraction

Call extract(template, templates?) to generate a strict JSON Schema.

Root structure

{
  "type": "object",
  "additionalProperties": false,
  "properties": { … },
  "required": [ … ],
  "$defs": { … }
}

Properties

Discovered from:

  • Content bindings (view-text, view-html)
  • Attribute bindings (view-attr:*)
  • Tag transforms (view-tagname)
  • Object scopes (view-object)
  • Arrays (view-each)
  • Declarations (view-declare)

All properties required by default.

Arrays

From view-each:

  • Items schema from children
  • Primitive inference when applicable
  • Constraints from view-items, view-min-items, view-max-items
  • Implicit sizes from indexed bindings

Composition

view-use:

  • Fragment object scopes → $defs/<fragmentName>
  • Usage property → { $ref: "#/$defs/<fragmentName>" }

view-stack:

  • items.oneOf → array of $refs
  • Each $defs entry includes is: { const: "..." }

Descriptions

Root: first top-level view-prompt (breadth-first)

Properties: view-prompt on same element or via view-prompt:[attr]

Arrays: view-prompt on <template view-each>

Objects: view-prompt on element with view-object

Types

No implicit numeric inference. Use view-type hints.

Boolean HTML attributes automatically infer boolean.


Validation

Templates validated at build/init time for fast runtime.

Errors reported

  • Content conflict: view-text and view-html together
  • Loop placement: view-each only on <template>
  • Loop/object combo: both on same <template>
  • Array constraints: invalid combinations, non-numeric values, min > max
  • Attribute identity: view-attr:* cannot use "" or "."
  • Tagname tokens: enum syntax must use [a-z][a-z0-9-]*
  • Fragments: missing templates, cycles
  • Prompts: identity context invalid, ambiguous targets
  • Union stacks: missing templates map, not on view-each

Validation result

{
  valid: boolean,
  errors: [
    {
      code: string,
      message: string,
      nodePath: string,  // CSS-like breadcrumb
      path?: string,      // data path
      attr?: string,      // attribute name
      fragment?: string   // source fragment
    }
  ]
}

JavaScript Integration

Two approaches: ad-hoc functions or pre-validated class.

Ad-hoc functions

Simplest entry point. Validates on every call.

render(template, data, options?, templates?)

import { render } from 'viewschema';

const html = render(
  '<h1 view-text="title">Default</h1>',
  { title: 'Hello' }
);

With fragments:

const templates = {
  card: '<article view-object="card">…</article>'
};

const html = render(
  '<template view-use="card"></template>',
  { card: { title: 'Title' } },
  undefined,
  templates
);

extract(html, templates?)

import { extract } from 'viewschema';

const schema = extract('<h1 view-text="title">Default</h1>');
// { type: "object", properties: { title: { type: "string", … } }, … }

validate(template, templates?)

import { validate } from 'viewschema';

const result = validate('<div view-text="x" view-html="y"></div>');
// { valid: false, errors: [{ code: "V_CONFLICT_CONTENT", … }] }

ViewSchema class

Pre-validates template map at construction. Fast render/extract thereafter.

Constructor

import { ViewSchema } from 'viewschema';

const vs = new ViewSchema({
  templates: {
    hero: '<section view-object="hero">…</section>',
    card: '<article view-object="card">…</article>',
    footer: '<footer>…</footer>'
  }
});

Validates all fragments:

  • Authoring rules
  • Missing references
  • Cycles

Throws on validation errors.

Render

vs.render(data, nameOrOptions?, options?)
// By name
const html = vs.render({ hero: { title: 'Hello' } }, 'hero');

// By data.is
const html = vs.render({ is: 'hero', hero: { title: 'Hello' } });

// With options
const html = vs.render(data, 'card', { validate: false });

Skips validation (already done at construction).

Extract

const schema = vs.extract('hero');

Fast schema generation for validated fragments.

Types

type TemplateMap = Record<string, string>;
type Data = Record<string, any>;

interface RenderOptions {
  validate?: boolean;  // Default true for functions, false for class
}

interface JSONSchema {
  type: 'object';
  properties: Record<string, JSONSchemaProperty>;
  required: string[];
  additionalProperties: boolean;
  $defs?: Record<string, JSONSchemaProperty>;
  description?: string;
}

interface JSONSchemaProperty {
  type?: 'string' | 'number' | 'boolean' | 'array' | 'object';
  description?: string;
  enum?: string[];
  minLength?: number;
  items?: JSONSchemaProperty;
  oneOf?: JSONSchemaProperty[];
  $ref?: string;
  properties?: Record<string, JSONSchemaProperty>;
  required?: string[];
  additionalProperties?: boolean;
  minItems?: number;
  maxItems?: number;
  const?: string;
}

Performance

ViewSchema is highly optimized with comprehensive caching for production use:

Benchmark Results:

  • Rendering: 114x faster on cache hits (3.8ms → 0.03ms)
  • Schema extraction: 2,750x faster on cache hits (instant after first call)
  • Throughput: 97,000+ renders per second sustained
  • Memory cost: ~1-5KB per unique template (negligible)

Optimization Features:

  • ✅ Global compilation cache (parse once, reuse forever)
  • ✅ Fast AST cloning (10-100x faster than re-parsing)
  • ✅ Property access caching (90% reduction in string operations)
  • ✅ Optimized HTML serialization (5-10x faster output)
  • ✅ Zero overhead on cache hits (<0.001ms lookup)

API Patterns:

Functions: validate on each call. Best for one-offs or prototyping. Still benefit from global caches.

Class: validates once at construction (including templates map). Best for production with instance-level caching.

Choose based on usage pattern. Class-based is highly recommended for applications with shared fragment libraries that use <template view-use|view-stack/> patterns.

Run benchmarks: npm test -- quick-bench --run

Read more: See BENCHMARK_RESULTS.md for detailed performance analysis.

Error messages

Representative examples:

  • "Template uses view-use but no templates map was provided"
  • "Missing template for view-use=\"hero\""
  • "Cyclic view-use detected: a -> b -> c -> a"
  • "Invalid template: view-text and view-html cannot be used together"
  • "view-each is intended for <template> elements"
  • "Use either view-items or view-min-items/view-max-items, but not both"
  • "Invalid attribute binding: identity or empty path is not allowed"
  • "view-stack must be used on a <template view-each=\"...\">"

Errors include:

  • nodePath: CSS-like element breadcrumb
  • path: data path involved
  • attr: attribute name
  • fragment: source fragment if applicable

Environment

Browser: DOMParser

Node.js: node-html-parser (dependency)

Tests: jsdom (dev dependency)


Examples

Minimal content binding

<h1 view-text="title">Default</h1>
render(template, { title: 'Hello World' });
// <h1>Hello World</h1>

Schema:

{
  "type": "object",
  "properties": {
    "title": { "type": "string", "minLength": 1 }
  },
  "required": ["title"],
  "additionalProperties": false
}

Object scope with attributes

<section view-object="cta">
  <a view-attr:href="url" view-text="label">Link</a>
</section>
const data = {
  cta: { url: '/start', label: 'Get started' }
};

Schema has cta object with url (string) and label (string) properties.

Array of objects

<ul>
  <template view-each="items">
    <li view-text="name">Item</li>
  </template>
</ul>
const data = {
  items: [
    { name: 'First' },
    { name: 'Second' }
  ]
};

Renders two <li> elements. Schema has items array with object items containing name property.

Primitive array

<template view-each="tags">
  <span view-text=""></span>
</template>
const data = { tags: ['html', 'css', 'js'] };

Schema infers items: { type: "string" }.

Fragment composition

const templates = {
  hero: `
    <section view-object="hero">
      <h1 view-text="title">Title</h1>
      <p view-text="subtitle">Subtitle</p>
      <a view-attr:href="cta.url" view-text="cta.label">Link</a>
    </section>
  `
};
<template view-use="hero" view-object="page.hero"></template>
const data = {
  page: {
    hero: {
      title: 'Welcome',
      subtitle: 'Start here',
      cta: { url: '/docs', label: 'Read docs' }
    }
  }
};

Schema emits $defs.hero with hero structure; page property references it via $ref.

Union stack

const templates = {
  widget: '<div><strong view-text="name">Widget</strong></div>',
  note: '<aside><p view-text="text">Note</p></aside>'
};
<template view-each="blocks" view-stack="widget|note"></template>
const data = {
  blocks: [
    { is: 'widget', name: 'Counter' },
    { is: 'note', text: 'Remember this' }
  ]
};

Schema:

{
  "properties": {
    "blocks": {
      "type": "array",
      "items": {
        "oneOf": [
          { "$ref": "#/$defs/widget" },
          { "$ref": "#/$defs/note" }
        ]
      }
    }
  },
  "$defs": {
    "widget": {
      "type": "object",
      "properties": {
        "is": { "type": "string", "const": "widget" },
        "name": { "type": "string", "minLength": 1 }
      },
      "required": ["is", "name"],
      "additionalProperties": false
    },
    "note": {
      "type": "object",
      "properties": {
        "is": { "type": "string", "const": "note" },
        "text": { "type": "string", "minLength": 1 }
      },
      "required": ["is", "text"],
      "additionalProperties": false
    }
  }
}

Tag transform with enum

<h2
  view-tagname="level=h1|h2|h3"
  view-prompt:level="Heading level"
  view-text="heading"
>
  Default Heading
</h2>
render(template, { level: 'h3', heading: 'Important' });
// <h3>Important</h3>

Schema: level: { type: "string", enum: ["h1", "h2", "h3"] }

Type hints and declarations

<div>
  <span view-type="number" view-text="age">0</span>
  <input view-declare="hiddenFlag" view-type="boolean" />
</div>

Schema:

  • age: { type: "number" }
  • hiddenFlag: { type: "boolean" }

Rendered output strips view-declare and view-type attributes.


Authoring checklist

Core directives

  • view-text for escaped text content
  • view-html for raw HTML (trusted only)
  • view-attr:[attr] for any attribute
  • view-object for nested data scope
  • <template view-each> for arrays
  • view-if for conditionals
  • view-tagname for dynamic elements

Schema documentation

  • view-prompt for property descriptions
  • view-prompt:[attr] for attribute descriptions
  • view-type for explicit types (no numeric inference)

Composition

  • <template view-use> for reusable fragments
  • view-stack for discriminated unions

Pitfalls to avoid

  • ✗ Don't mix view-text and view-html
  • ✗ Don't put view-each on non-<template>
  • ✗ Don't combine view-each and view-object on same element
  • ✗ Don't use identity paths ("", ".") for view-attr:*
  • ✗ Don't use cycles in fragments

Boolean attributes (auto-typed)

disabled, checked, selected, readonly, required, multiple, autofocus, hidden, open, reversed, async, defer, novalidate, formnovalidate

Metadata stripped at render

view-prompt, view-prompt:, view-type, view-type:, view-declare, view-from


Workflows by role

HTML template author

  1. Start with semantic HTML
  2. Add view-text for content, view-attr:* for attributes
  3. Use view-object to scope nested data
  4. Use <template view-each> for lists
  5. Add view-prompt descriptions
  6. Add view-type hints for non-strings
  7. Extract reusable pieces with <template view-use>
  8. Model heterogeneous lists with view-stack

Validate early with validateTemplate or let the integrator's ViewSchema constructor catch errors.

JavaScript integrator

Development

import { render, extract } from 'viewschema';

const html = render(template, data, undefined, templates);
const schema = extract(template, templates);

Production

import { ViewSchema } from 'viewschema';

// Validate once at startup
const vs = new ViewSchema({ templates });

// Fast render
app.get('/render/:name', (req, res) => {
  const html = vs.render(req.body.data, req.params.name);
  res.send(html);
});

// Publish schemas
const schemas = {};
for (const name of Object.keys(templates)) {
  schemas[name] = vs.extract(name);
}

Error handling

try {
  const vs = new ViewSchema({ templates });
} catch (err) {
  // Validation failed: missing fragments, cycles, authoring errors
  console.error(err.message);
}

FAQ

Why no numeric inference?

Predictability. Templates should be explicit. Use view-type="number" when needed.

Can I bind the whole context?

Yes: view-text="." or view-html=".". Objects stringify to JSON (empty → blank).

How do I document properties?

view-prompt="Description" on the element. For attributes: view-prompt:href="URL description".

What about security?

view-text escapes HTML automatically. view-html is raw — sanitize your data.

Can fragments be nested?

Yes. Fragments can reference other fragments. Cycles are detected and rejected.

What happens to defaults?

Default content (text/children in template) is cleared when bindings apply.

How do I ensure strict arrays?

Use view-items="N" for exact count, or use indexed bindings (items.0.field, items.1.field) for implicit sizing.

Can I transform tags safely?

Yes with view-tagname. Use enum syntax (tag=h1|h2|h3) for validation. Tokens must match [a-z][a-z0-9-]*.

What about TypeScript?

Full type definitions included. Import types: JSONSchema, TemplateMap, Data, RenderOptions.