viewschema
v0.0.6
Published
Template engine for dynamic HTML with schema-driven data binding
Downloads
23
Maintainers
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=""orview-text="." - Schema:
stringproperty withminLength: 1by 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=""orview-markdown="." - Schema:
stringproperty withminLength: 1by default - Automatic prompt: When no
view-promptis 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 (
) - Blockquotes (
> text) - Unordered lists (
- itemor* 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
srcremoves any existingsrcsetto avoid conflicts - Identity path not allowed
- Schema: boolean attributes infer
type: boolean; others default tostring
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
objectproperty; children populateproperties
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:
arrayproperty; children determineitemsschema
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-itemsand/orview-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-eachmust be on<template>- Cannot combine
view-eachandview-objecton 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-objectscope - 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:
stringproperty, withenumif 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:*orview-tagname) - Min cannot exceed max
- Pattern must be valid regex
- Variance must be 0-100% or 0-1 fraction
multipleOfmust 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
stringwithminLength: 1 - Use
view-typeto 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 hasinner→ fragment becomesouter.inner - Fragment root has no
view-object→ adoptsouter - Identity
.→ adopts current scope at runtime
Combining with loops
<template view-each="cards" view-use="card"></template>Schema behavior
- Fragment object scopes emit to
$defskeyed 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:
- Expand fragments (
view-use) - Expand loops (
view-each) - Apply object scopes (
view-object) - Apply conditionals (
view-if) - Transform tags (
view-tagname) - Bind content (
view-text,view-html) - Bind attributes (
view-attr:*) - Strip metadata (
view-prompt,view-type,view-declare, internal markers)
Elements removed when:
view-ifevaluates falsyview-objectdoesn'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
$defsentry includesis: { 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-textandview-htmltogether - Loop placement:
view-eachonly 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 breadcrumbpath: data path involvedattr: attribute namefragment: 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-textfor escaped text content - ✓
view-htmlfor raw HTML (trusted only) - ✓
view-attr:[attr]for any attribute - ✓
view-objectfor nested data scope - ✓
<template view-each>for arrays - ✓
view-iffor conditionals - ✓
view-tagnamefor dynamic elements
Schema documentation
- ✓
view-promptfor property descriptions - ✓
view-prompt:[attr]for attribute descriptions - ✓
view-typefor explicit types (no numeric inference)
Composition
- ✓
<template view-use>for reusable fragments - ✓
view-stackfor discriminated unions
Pitfalls to avoid
- ✗ Don't mix
view-textandview-html - ✗ Don't put
view-eachon non-<template> - ✗ Don't combine
view-eachandview-objecton same element - ✗ Don't use identity paths (
"",".") forview-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
- Start with semantic HTML
- Add
view-textfor content,view-attr:*for attributes - Use
view-objectto scope nested data - Use
<template view-each>for lists - Add
view-promptdescriptions - Add
view-typehints for non-strings - Extract reusable pieces with
<template view-use> - 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.
