htmlc

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 9, 2026 License: MIT Imports: 22 Imported by: 0

README

htmlc

A server-side Go template engine that uses Vue.js Single File Component (.vue) syntax for authoring but renders entirely in Go with no JavaScript runtime.

This is a static rendering engine. There is no reactivity, virtual DOM, or client-side hydration. Templates are evaluated once per request and produce plain HTML.


Table of Contents

  1. Overview
  2. Template Syntax
  3. Directives
  4. Component System
  5. Special Attributes
  6. Go API Quick Reference
  7. Expression Language Reference

1. Overview

htmlc lets you write reusable HTML components in .vue files — the same format used by Vue.js — and render them server-side in Go. There is no Node.js dependency and no JavaScript executed at runtime. The <script> section of a .vue file is parsed and preserved in the output but never executed by the engine.

Key characteristics:

  • Static output — every render call produces a fixed HTML string.
  • Scoped styles<style scoped> is supported; the engine rewrites selectors and injects scope attributes automatically.
  • Component composition — components can nest other components from the same registry.
  • No reactivityv-model, @event, and other client-side directives are stripped from the output; they have no meaning in a server-side renderer.

2. Template Syntax

Supported
Text interpolation

{{ expr }} evaluates the expression against the current render scope and HTML-escapes the result.

<p>Hello, {{ name }}!</p>
<p>{{ a }} + {{ b }} = {{ a + b }}</p>

Multiple interpolations in a single text node are supported.

Expression language
Category Operators / Syntax
Arithmetic +, -, *, /, %, **
Comparison ===, !==, >, <, >=, <=, ==, !=
Logical &&, ||, !
Nullish coalescing ??
Ternary condition ? then : else
Member access obj.key, arr[i], arr.length
Function calls fn(args) (via expr.RegisterBuiltin)
Array literals [a, b, c]
Object literals { key: value }
Built-in functions

The engine ships with no pre-registered built-in functions. Use expr.RegisterBuiltin to add custom functions that are callable from templates by name. For measuring collection sizes, use the .length member property instead — it works on strings, slices, arrays, and maps with no registration required:

<!-- number of elements in a slice -->
<span>{{ items.length }}</span>

<!-- number of bytes in a string -->
<span>{{ name.length }}</span>
Not supported
  • Filters ({{ value | filterName }}) — Vue 2 syntax, not implemented.
  • JavaScript function definitions, arrow functions (=>), new, delete.
  • Template literals (backtick strings).
  • Optional chaining (?.).
  • Assignment operators (=, +=, etc.) and increment/decrement (++, --).

3. Directives

Supported directives
Directive Supported Notes
v-text Yes Sets element text content (HTML-escaped). Replaces all children.
v-html Yes Sets element inner HTML (not escaped). Replaces all children. Use with trusted content only.
v-show Yes Adds style="display:none" when the expression is falsy. Merges with any existing style attribute.
v-if Yes Renders the element only when the expression is truthy.
v-else-if Yes Must immediately follow a v-if or v-else-if element (whitespace between is allowed).
v-else Yes Must immediately follow a v-if or v-else-if element.
v-for Yes See v-for syntax below.
v-bind / :attr Yes Dynamic attribute binding. See v-bind notes below.
v-pre Yes Skips all interpolation and directive processing for the element and all its descendants. The v-pre attribute itself is stripped from the output.
v-slot / #name Yes Used on <template> elements (or directly on a component tag) to target named or scoped slots. Shorthand: #name. See Slots under §4.
v-once No-op Accepted and stripped; server-side rendering always renders once, so this directive has no effect.
Not supported
Directive Status
v-on / @event Stripped. Client-side event handlers have no meaning in server-side rendering.
v-model Stripped. Two-way data binding has no meaning in server-side rendering.
v-cloak Not relevant for server-side rendering.
v-memo Not implemented.
v-for syntax
<!-- Array: item only -->
<li v-for="item in items">{{ item }}</li>

<!-- Array: item + index -->
<li v-for="(item, index) in items">{{ index }}: {{ item }}</li>

<!-- Map: value + key -->
<li v-for="(value, key) in obj">{{ key }}: {{ value }}</li>

<!-- Map: value + key + index (index is position in iteration order) -->
<li v-for="(value, key, index) in obj">{{ index }}. {{ key }}: {{ value }}</li>

<!-- Integer range: iterates 1..n inclusive -->
<li v-for="n in 5">{{ n }}</li>

<!-- Multi-element group using <template> -->
<template v-for="item in items">
  <dt>{{ item.term }}</dt>
  <dd>{{ item.def }}</dd>
</template>

Difference from Vue.js: Map iteration order follows Go's reflect.MapKeys() order, which is not guaranteed to be insertion order.

v-bind notes
  • :class supports object syntax ({ active: isActive }) and array syntax ([classA, classB]).
  • :style supports object syntax with camelCase keys ({ fontSize: '14px' }); keys are converted to kebab-case in the output.
  • Boolean attributes (disabled, checked, selected, readonly, required, multiple, autofocus, open) are omitted entirely when the bound value is falsy.
  • :key is rendered as data-key="value" in the output (not as a key attribute).
  • class and :class are merged into a single class attribute.
  • style and :style are merged into a single style attribute.
  • v-bind:attr (long form) is equivalent to :attr (shorthand).

4. Component System

Supported
Single File Components

A .vue file may have three top-level sections:

<template>
  <!-- required; HTML template -->
</template>

<script>
  // optional; preserved verbatim in output but NOT executed
</script>

<style>
  /* optional; collected and injected as a <style> block */
</style>
Props

Pass data to child components via attributes:

<!-- Dynamic prop (expression evaluated in caller scope) -->
<Card :title="pageTitle" :count="items.length" />

<!-- Static prop (always a string) -->
<Card title="Hello" />

No prop type validation or default values — the engine passes whatever you provide.

Default slot

Use <slot /> inside a component to render the caller's inner content:

<!-- Card.vue -->
<template>
  <div class="card">
    <slot />
  </div>
</template>
<!-- caller -->
<Card>
  <p>This goes into the slot.</p>
</Card>

Slot content is evaluated in the caller's scope, not the child component's scope.

Slots
Default slot

As shown above, <slot /> renders the caller's inner content. Children of <slot> act as fallback content — rendered only when the caller provides nothing:

<!-- Button.vue -->
<template>
  <button>
    <slot>Click me</slot>
  </button>
</template>
<!-- renders "Click me" because no content provided -->
<Button />

<!-- renders "Submit" -->
<Button>Submit</Button>
Named slots

A component can expose multiple insertion points by giving each <slot> a name attribute. The caller targets a named slot with <template v-slot:name> or the # shorthand <template #name>:

<!-- Layout.vue -->
<template>
  <div class="layout">
    <header><slot name="header" /></header>
    <main><slot /></main>
    <footer><slot name="footer" /></footer>
  </div>
</template>
<!-- caller -->
<Layout>
  <template #header><h1>{{ pageTitle }}</h1></template>
  <p>Main body content.</p>
  <template #footer><small>© 2024</small></template>
</Layout>

Content without a v-slot / # target goes to the default slot.

Scoped slots

A component can pass data back to the caller's slot content by binding props on the <slot> element. The caller receives them via v-slot="{ … }" or #name="{ … }":

<!-- List.vue -->
<template>
  <ul>
    <li v-for="item in items">
      <slot :item="item" :index="index" />
    </li>
  </ul>
</template>
<!-- caller — destructured binding -->
<List :items="products">
  <template #default="{ item, index }">
    <strong>{{ index }}.</strong> {{ item.name }}
  </template>
</List>

Binding patterns:

Syntax Effect
v-slot Slot targeted, no props exposed
v-slot="slotProps" All slot props available as slotProps.x
v-slot="{ item }" Destructured; item available directly
v-slot="{ item, index }" Multiple destructured props
Scope rules
  • Slot content is always evaluated in the caller's scope.
  • Slot props (from :prop="expr" on <slot>) are merged into the scope when rendering that slot's content — they do not leak into the rest of the caller's template.
  • Named-slot props are scoped to the <template #name="…"> block.
Component resolution

Given a tag name, the engine tries these strategies in order:

  1. Exact match in the registry (e.g. my-cardmy-card)
  2. First letter capitalised (e.g. cardCard)
  3. Kebab-case to PascalCase (e.g. my-cardMyCard)
  4. Case-insensitive scan
Scoped styles
<style scoped>
.button { color: red; }
</style>

The engine rewrites CSS selectors with a data-v-* scope attribute (e.g. .button[data-v-abc123]) and adds that attribute to every HTML element rendered by the component.

Nested composition

Components can freely use other components registered in the same engine.

Dynamic components

Use <component :is="expr"> to render a component whose name is determined at runtime. The expression must evaluate to a non-empty string that names a registered component or a native HTML element:

<!-- resolve from a variable -->
<component :is="activeView" />

<!-- inline string literal -->
<component :is="'Card'" :title="pageTitle">
  <p>slot content</p>
</component>

<!-- switch between components in a loop -->
<div v-for="item in items">
  <component :is="item.type" :data="item" />
</div>
  • All attributes other than :is (or v-bind:is) are forwarded to the resolved component as props.
  • Slot content (default and named) works exactly as with a statically-named component.
  • If the resolved name is a known HTML element (e.g. "div", "input"), the tag is rendered as-is rather than looked up in the component registry.
  • :is is required; omitting it or supplying a non-string value is a render error.
Not supported
Feature Status
<script setup> / Composition API Not supported. <script> content is never executed.
Computed properties, watchers, lifecycle hooks Not applicable (no runtime).
$emit / custom events Not implemented.
provide / inject Not implemented.
Async components Not applicable.
defineProps / defineEmits / withDefaults Not applicable.
Teleport, Suspense, KeepAlive Not applicable.

5. Special Attributes

Attribute Behavior
:key Rendered as data-key="value" in the HTML output. Not used for diffing.
class + :class Both are collected and merged into a single class attribute.
style + :style Both are collected and merged into a single style attribute.

6. Go API Quick Reference

Create an engine
engine, err := htmlc.New(htmlc.Options{
    ComponentDir: "templates/",  // recursively scanned for *.vue files
    Reload:       false,         // set true for hot-reload during development
})
Render a full HTML page

Scoped styles are injected before the first </head> tag.

err = engine.RenderPage(w, "Page", map[string]any{
    "title": "Home",
    "items": []string{"a", "b"},
})
Render an HTML fragment

Scoped styles are prepended before the HTML. Use this for HTMX responses, turbo frames, etc.

err = engine.RenderFragment(w, "Card", map[string]any{
    "title": "My Card",
})
Serve a component as an HTTP handler
http.Handle("/widget", engine.ServeComponent("Widget", func(r *http.Request) map[string]any {
    return map[string]any{"id": r.URL.Query().Get("id")}
}))

Pass nil as the second argument if the component needs no data.

Parse a component manually
comp, err := htmlc.ParseFile("path/to/Button.vue", srcString)
Discover expected props
for _, p := range comp.Props() {
    fmt.Println(p.Name, p.Expressions)
}
Configure missing prop behavior

By default, a missing prop causes a render error. Use WithMissingPropHandler to substitute a value instead:

engine.WithMissingPropHandler(htmlc.SubstituteMissingProp)
// or provide your own:
engine.WithMissingPropHandler(func(name string) (any, error) {
    return "", nil  // silently substitute empty string
})
Development hot-reload
engine, err := htmlc.New(htmlc.Options{
    ComponentDir: "templates/",
    Reload:       true,  // re-parses changed files before each render
})
Load components from an embedded filesystem

Set Options.FS to any fs.FS implementation — including embed.FS — and the engine reads and walks component files through that FS instead of the OS filesystem. ComponentDir is then interpreted as a path within the FS.

import "embed"

//go:embed templates
var templateFS embed.FS

engine, err := htmlc.New(htmlc.Options{
    FS:           templateFS,
    ComponentDir: "templates",
})

This embeds the entire templates/ directory into the binary at compile time. Any fs.FS implementation works — embed.FS, testing/fstest.MapFS, fs.Sub, or a custom virtual filesystem.

Hot-reload (Reload: true) is supported when the FS implements fs.StatFS (which embed.FS does not — embedded files have no mtime). When the FS does not implement fs.StatFS, reload checks are silently skipped.


7. Expression Language Reference

Expressions are JavaScript-compatible in syntax and truthiness rules but are evaluated entirely in Go.

Operators (highest to lowest precedence)
Precedence Operators Example
7 (highest) Unary !, unary - !active, -x
6 ** (exponentiation) 2 ** 10
5 *, /, % price * qty
4 +, - a + b
3 >, <, >=, <=, ==, !=, ===, !== count > 0
2 && a && b
2 ||, ?? a || 'default', val ?? 'n/a'
1 (lowest) ? : (ternary) ok ? 'yes' : 'no'

Member access (obj.key, arr[i]) and function calls (fn(args)) have the highest binding and are parsed as primary expressions.

Truthiness (JavaScript-compatible)

Falsy values: false, 0, "" (empty string), null, undefined. Everything else is truthy, including empty arrays and empty objects.

Type notes
  • All numbers are float64 internally (JavaScript number semantics).
  • Accessing a missing map key or an out-of-range index returns undefined (not an error).
  • null and undefined are distinct values. null == undefined is true; null === undefined is false.
  • The ?? operator returns the right-hand side only when the left-hand side is null or undefined (not when it is 0 or "").
Examples
{{ count > 0 ? count : "none" }}
{{ user.name ?? "Guest" }}
{{ items[0].title }}
{{ tags.length }}
{{ price * 1.2 }}
{{ active ? "active" : "" }}

Documentation

Overview

Package htmlc is a server-side Vue-style component engine for Go. It parses .vue Single File Components — each containing a <template>, an optional <script>, and an optional <style> section — and renders them to HTML strings ready to serve via net/http or any http.Handler-based framework.

Problem it solves

Writing server-rendered HTML in Go typically means either concatenating strings, using html/template (which has no component model), or pulling in a full JavaScript runtime. htmlc gives you Vue's familiar component authoring format — scoped styles, template directives, and component composition — while keeping rendering purely in Go with no JavaScript dependency.

Mental model

There are four main concepts:

  • Engine – the high-level entry point. It owns a Registry of parsed components discovered from a directory tree. Create one with New; call RenderPage or RenderFragment to produce HTML output. ServeComponent wraps a component as an http.Handler for use with net/http.

  • Component – the parsed representation of one .vue file, produced by ParseFile. Holds the template node tree, script text, style text, and scoped-style metadata.

  • Renderer – the low-level walker that evaluates a Component's template against a data scope and writes HTML. Most callers should use Engine instead.

  • StyleCollector – accumulates scoped-style contributions from all components rendered in one request so they can be emitted as a single <style> block at the end.

Template directives

Every directive below is processed server-side. Client-only directives (@click, v-model) are stripped from the output because there is no JavaScript runtime.

{{ expr }}
    Mustache text interpolation; the expression result is HTML-escaped.
    Example: <p>{{ user.name }}</p>

v-text="expr"
    Sets element text content; HTML-escaped; replaces any child nodes.
    Example: <p v-text="msg"></p>

v-html="expr"
    Sets element inner HTML; the value is NOT HTML-escaped.
    Example: <div v-html="rawHTML"></div>

v-if / v-else-if / v-else
    Conditional rendering; only the first truthy branch is emitted.
    Example: <span v-if="score >= 90">A</span>
             <span v-else-if="score >= 70">B</span>
             <span v-else>C</span>

v-for="item in items"
    Iterate over a slice or array. Use (item, i) in items for zero-based
    index access.
    Example: <li v-for="(item, i) in items">{{ i }}: {{ item }}</li>

v-for="n in N"
    Integer range: n iterates 1 … N (inclusive).
    Example: <span v-for="n in 3">{{ n }}</span>

v-for="(val, key) in obj"
    Iterate map entries; val is the value, key is the string key.
    Example: <dt v-for="(val, key) in obj">{{ key }}: {{ val }}</dt>

:attr="expr"
    Dynamic attribute binding. Boolean attributes are omitted when the
    expression is falsy, present without a value when truthy.
    Example: <a :href="url">link</a>

:class="{ key: bool }"
    Object-syntax class binding: keys whose values are truthy are
    included; merged with any static class attribute.
    Example: <div class="base" :class="{ active: isActive }">…</div>

:class="[...]"
    Array-syntax class binding: non-empty string elements are included.
    Example: <div :class="['btn', flag ? 'primary' : '']">…</div>

:style="{ camelKey: val }"
    Inline style binding; camelCase keys are converted to kebab-case.
    Example: <p :style="{ fontSize: '14px', color: 'red' }">…</p>

v-show="expr"
    Adds style="display:none" when the expression is falsy; the element
    is always present in the output.
    Example: <p v-show="visible">content</p>

v-pre
    Skips all interpolation and directive processing for the subtree;
    mustache syntax is emitted literally.
    Example: <code v-pre>{{ raw }}</code>

@click, v-model
    Client-side event and model directives; stripped on server render.
    Example: <button @click="handler">click</button>

Component composition

Components can include other components in their templates. A child component name must start with an uppercase letter to distinguish it from HTML elements.

## Registering components

There are two ways to make components available for composition:

1. Automatic discovery via ComponentDir: every .vue file in the directory tree is registered under its basename (without the .vue extension).

engine, err := htmlc.New(htmlc.Options{ComponentDir: "templates/"})

2. Manual registration via Engine.Register or by constructing a Registry directly and passing it to NewRenderer.WithComponents.

engine.Register("Card", cardComponent)

// Low-level API:
htmlc.NewRenderer(page).WithComponents(htmlc.Registry{"Card": card})

## Default slot

A child component declares <slot /> as a placeholder. The parent places inner HTML inside the component tag and it is injected at the slot site.

Card.vue:

<template><div class="card"><slot /></div></template>

Page.vue:

<template><Card><p>inner</p></Card></template>

Renders to: <div class="card"><p>inner</p></div>

## Named slots

A child can declare multiple slots by name using <slot name="…">. The parent fills each slot with a <template #name> element. Unmatched content goes to the default slot.

Layout.vue:

<template>
  <div class="layout">
    <slot name="header"></slot>
    <main><slot></slot></main>
    <slot name="footer"></slot>
  </div>
</template>

Page.vue:

<template>
  <Layout>
    <template #header><h1>Title</h1></template>
    <p>Content</p>
    <template #footer><em>Footer</em></template>
  </Layout>
</template>

## Scoped slots

The child passes data up to the parent via slot props. The parent destructures the props with v-slot="{ item, index }" or binds the whole map with v-slot="props".

List.vue:

<template>
  <ul>
    <li v-for="(item, i) in items">
      <slot :item="item" :index="i"></slot>
    </li>
  </ul>
</template>

Page.vue (destructuring):

<template>
  <List :items="items" v-slot="{ item, index }">
    <span>{{ index }}: {{ item }}</span>
  </List>
</template>

Page.vue (whole map bound to a variable):

<template>
  <Child v-slot="props"><p>{{ props.user.name }}</p></Child>
</template>

## Slot fallback content

Children placed inside a <slot> element in the child component are rendered when the parent provides no content for that slot.

Card.vue:

<template>
  <div class="card"><slot><p>No content provided</p></slot></div>
</template>

Page.vue (no slot content supplied):

<template><Card></Card></template>

Renders to: <div class="card"><p>No content provided</p></div>

Scoped styles

Adding <style scoped> to a .vue file generates a unique data-v-XXXXXXXX attribute (derived from the file path) that is stamped on every element rendered by that component. The CSS is rewritten by ScopeCSS so every selector targets only elements bearing that attribute.

When using Engine, styles are collected automatically:

  • RenderPage injects a <style> block immediately before </head>.
  • RenderFragment prepends the <style> block to the output.

When using the low-level API, manage styles manually:

sc := &htmlc.StyleCollector{}
out, err := htmlc.NewRenderer(comp).WithStyles(sc).RenderString(nil)
items := sc.All() // []*htmlc.StyleItem; each has a CSS field

Missing prop handling

By default, a prop name that appears in the template but is absent from the scope map causes a render error. Supply a handler to override this behaviour:

// Engine-wide:
engine.WithMissingPropHandler(htmlc.SubstituteMissingProp)

// Per-render with the low-level API:
out, err := htmlc.NewRenderer(comp).
    WithMissingPropHandler(htmlc.SubstituteMissingProp).
    RenderString(nil)

SubstituteMissingProp is the built-in handler; it emits "MISSING PROP: <name>" in place of the missing value. It is useful during development to surface missing data without aborting the render.

Embedded filesystems

Components can be loaded from an embedded filesystem using go:embed:

//go:embed templates
var templateFS embed.FS

engine, err := htmlc.New(htmlc.Options{
    FS:           templateFS,
    ComponentDir: "templates",
})

Note: Engine.Reload only works when the fs.FS also implements fs.StatFS. The standard os.DirFS satisfies this; embed.FS does not, so Reload is a no-op for embedded filesystems.

Low-level API

Use ParseFile, NewRenderer, WithComponents, WithStyles, and WithDirectives directly when you need request-scoped control over component registration, style collection, or custom directives — for example, in tests or one-off renders outside of a long-lived Engine.

// Parse two components from strings.
card, _ := htmlc.ParseFile("Card.vue", `<template><div class="card"><slot /></div></template>`)
page, _ := htmlc.ParseFile("Page.vue", `<template><Card><p>inner</p></Card></template>`)

// Collect scoped styles while rendering.
sc := &htmlc.StyleCollector{}
out, err := htmlc.NewRenderer(page).
    WithComponents(htmlc.Registry{"Card": card}).
    WithStyles(sc).
    RenderString(nil)
if err != nil { /* handle */ }

// Retrieve scoped CSS generated during the render.
for _, item := range sc.All() {
    fmt.Println(item.CSS)
}

Custom directives

htmlc supports a custom directive system inspired by Vue's custom directives (https://vuejs.org/guide/reusability/custom-directives). Types that implement the Directive interface can be registered under a v-name attribute and are invoked during server-side rendering.

Only the Created and Mounted hooks are supported because htmlc renders server-side only — there are no DOM updates or browser events.

  • Created – called before the element is rendered; may mutate the element's tag (node.Data) and attributes (node.Attr).
  • Mounted – called after the element's closing tag has been written; may write additional HTML to the output writer.

Register directives via Options.Directives or Engine.RegisterDirective:

engine, err := htmlc.New(htmlc.Options{
    ComponentDir: "templates/",
    Directives: htmlc.DirectiveRegistry{
        "switch": &htmlc.VSwitch{},
    },
})

The built-in VSwitch directive is the canonical example: it replaces the host element's tag with a registered component name supplied by the directive's expression, enabling dynamic component dispatch.

Tutorial

The fastest path to a working server is Engine + RenderPage:

engine, err := htmlc.New(htmlc.Options{ComponentDir: "templates/"})
if err != nil { /* handle */ }

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    if err := engine.RenderPage(w, "Page", map[string]any{"title": "Home"}); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
})

Use RenderPage when the component template is a full HTML document (html/head/body); it injects collected <style> blocks before </head>. Use RenderFragment (or RenderFragmentString) for partial HTML snippets — for example, components rendered inside an existing layout or delivered over HTMX.

For development, enable hot-reload so changes to .vue files are picked up without restarting the server:

engine, err := htmlc.New(htmlc.Options{
    ComponentDir: "templates/",
    Reload:       true,
})
Example

Example demonstrates end-to-end use of the htmlc engine: create an Engine from a directory of .vue files, then render a component as an HTML fragment.

package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/dhamidi/htmlc"
)

func main() {
	dir, err := os.MkdirTemp("", "htmlc-example-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(dir)

	// Write a simple component with no scoped styles so the output is stable.
	vue := `<template><p>Hello, {{ name }}!</p></template>`
	if err := os.WriteFile(filepath.Join(dir, "Greeting.vue"), []byte(vue), 0644); err != nil {
		log.Fatal(err)
	}

	engine, err := htmlc.New(htmlc.Options{ComponentDir: dir})
	if err != nil {
		log.Fatal(err)
	}

	out, err := engine.RenderFragmentString("Greeting", map[string]any{"name": "World"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<p>Hello, World!</p>

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrComponentNotFound is returned when the requested component name is not
	// registered in the engine.
	ErrComponentNotFound = errors.New("htmlc: component not found")

	// ErrMissingProp is returned when a required prop is absent from the render
	// scope and no MissingPropFunc has been set.
	ErrMissingProp = errors.New("htmlc: missing required prop")
)

Sentinel errors returned by Engine methods.

Functions

func Render

func Render(w io.Writer, c *Component, scope map[string]any) error

Render is a convenience wrapper that creates a temporary Renderer for c and writes the rendered HTML directly to w. It does not collect styles or support component composition; use NewRenderer with WithStyles and WithComponents for those features.

Example (ComponentProps)

ExampleRender_componentProps shows a parent passing a dynamic prop via :title="expr" and a static string prop via class="x" to a child component.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	header, _ := htmlc.ParseFile("Header.vue", `<template><h1>{{ title }}</h1></template>`)
	page, _ := htmlc.ParseFile("Page.vue", `<template><Header :title="heading" class="main"></Header></template>`)
	out, err := htmlc.NewRenderer(page).
		WithComponents(htmlc.Registry{"Header": header}).
		RenderString(map[string]any{"heading": "My Site"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<h1>My Site</h1>
Example (ComponentSlot)

ExampleRender_componentSlot shows a parent component passing inner HTML content into a child component's <slot /> placeholder.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	card, _ := htmlc.ParseFile("Card.vue", `<template><div class="card"><slot /></div></template>`)
	page, _ := htmlc.ParseFile("Page.vue", `<template><Card><p>inner</p></Card></template>`)
	out, err := htmlc.NewRenderer(page).
		WithComponents(htmlc.Registry{"Card": card}).
		RenderString(nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<div class="card"><p>inner</p></div>
Example (EventPassthrough)

ExampleRender_eventPassthrough shows that client-side directives such as @click and v-model are stripped from server-rendered output because there is no client-side Vue runtime to process them.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	const src = `<template><button @click="handler">click</button><input v-model="name"></template>`
	comp, err := htmlc.ParseFile("t.vue", src)
	if err != nil {
		log.Fatal(err)
	}
	out, err := htmlc.RenderString(comp, nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<button>click</button><input>
Example (Interpolation)

ExampleRender_interpolation shows {{ expr }} text interpolation: member access, arithmetic, and ternary expressions all evaluated at render time.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	const src = `<template><p>{{ user.name }}, total {{ price * qty }}, active: {{ active ? "yes" : "no" }}</p></template>`
	comp, err := htmlc.ParseFile("t.vue", src)
	if err != nil {
		log.Fatal(err)
	}
	out, err := htmlc.RenderString(comp, map[string]any{
		"user":   map[string]any{"name": "Alice"},
		"price":  float64(10),
		"qty":    float64(3),
		"active": true,
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<p>Alice, total 30, active: yes</p>
Example (NamedSlots)

ExampleRender_namedSlots demonstrates named slots: a Layout component declares header and footer named slots plus a default slot for body content, and the caller fills each slot with a <template #name> element.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	layout, _ := htmlc.ParseFile("Layout.vue",
		`<template><div class="layout"><slot name="header"></slot><main><slot></slot></main><slot name="footer"></slot></div></template>`)
	page, _ := htmlc.ParseFile("Page.vue",
		`<template><Layout><template #header><h1>Title</h1></template><p>Content</p><template #footer><em>Footer</em></template></Layout></template>`)
	out, err := htmlc.NewRenderer(page).
		WithComponents(htmlc.Registry{"Layout": layout}).
		RenderString(nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<div class="layout"><h1>Title</h1><main><p>Content</p></main><em>Footer</em></div>
Example (ScopedSlots)

ExampleRender_scopedSlots demonstrates scoped slots: a List component passes each item and its index to the caller via slot props, and the caller uses v-slot="{ item, index }" to render a custom item template.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	list, _ := htmlc.ParseFile("List.vue",
		`<template><ul><li v-for="(item, i) in items"><slot :item="item" :index="i"></slot></li></ul></template>`)
	page, _ := htmlc.ParseFile("Page.vue",
		`<template><List :items="items" v-slot="{ item, index }"><span>{{ index }}: {{ item }}</span></List></template>`)
	out, err := htmlc.NewRenderer(page).
		WithComponents(htmlc.Registry{"List": list}).
		RenderString(map[string]any{"items": []any{"a", "b"}})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<ul><li><span>0: a</span></li><li><span>1: b</span></li></ul>
Example (SingleVariableSlotBinding)

ExampleRender_singleVariableSlotBinding demonstrates v-slot="slotProps" binding: the entire slot props map is bound to a single variable, allowing the caller to access any prop via slotProps.key.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	child, _ := htmlc.ParseFile("Child.vue",
		`<template><div><slot :user="theuser" :count="total"></slot></div></template>`)
	page, _ := htmlc.ParseFile("Page.vue",
		`<template><Child :theuser="u" :total="n" v-slot="props"><p>{{ props.user.name }}: {{ props.count }}</p></Child></template>`)
	out, err := htmlc.NewRenderer(page).
		WithComponents(htmlc.Registry{"Child": child}).
		RenderString(map[string]any{
			"u": map[string]any{"name": "Alice"},
			"n": float64(3),
		})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<div><p>Alice: 3</p></div>
Example (SlotFallbackContent)

ExampleRender_slotFallbackContent demonstrates slot fallback content: when the caller provides no content for a slot, the child component's fallback children inside <slot>…</slot> are rendered instead.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	card, _ := htmlc.ParseFile("Card.vue",
		`<template><div class="card"><slot><p>No content provided</p></slot></div></template>`)
	page, _ := htmlc.ParseFile("Page.vue", `<template><Card></Card></template>`)
	out, err := htmlc.NewRenderer(page).
		WithComponents(htmlc.Registry{"Card": card}).
		RenderString(nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<div class="card"><p>No content provided</p></div>
Example (VBind)

ExampleRender_vBind shows :attr dynamic attribute binding, and how boolean attributes are handled: :disabled="false" omits the attribute entirely while :disabled="true" emits it without a value.

package main

import (
	"fmt"

	"github.com/dhamidi/htmlc"
)

func main() {
	// Dynamic href binding.
	c1, _ := htmlc.ParseFile("t.vue", `<template><a :href="url">link</a></template>`)
	o1, _ := htmlc.RenderString(c1, map[string]any{"url": "https://example.com"})
	fmt.Println(o1)

	// :disabled="false" — attribute is omitted.
	c2, _ := htmlc.ParseFile("t.vue", `<template><button :disabled="false">enabled</button></template>`)
	o2, _ := htmlc.RenderString(c2, nil)
	fmt.Println(o2)

	// :disabled="true" — attribute is present without a value.
	c3, _ := htmlc.ParseFile("t.vue", `<template><button :disabled="true">disabled</button></template>`)
	o3, _ := htmlc.RenderString(c3, nil)
	fmt.Println(o3)
}
Output:
<a href="https://example.com">link</a>
<button>enabled</button>
<button disabled>disabled</button>
Example (VBindClass)

ExampleRender_vBindClass shows :class with object syntax (keys whose values are truthy are included) and array syntax, both merged with a static class.

package main

import (
	"fmt"

	"github.com/dhamidi/htmlc"
)

func main() {
	// Object syntax merged with static class="base".
	c1, _ := htmlc.ParseFile("t.vue", `<template><div class="base" :class="{ active: isActive, hidden: isHidden }">x</div></template>`)
	o1, _ := htmlc.RenderString(c1, map[string]any{"isActive": true, "isHidden": false})
	fmt.Println(o1)

	// Array syntax: non-empty strings are included.
	c2, _ := htmlc.ParseFile("t.vue", `<template><div :class="['btn', flag ? 'primary' : '']">y</div></template>`)
	o2, _ := htmlc.RenderString(c2, map[string]any{"flag": true})
	fmt.Println(o2)
}
Output:
<div class="base active">x</div>
<div class="btn primary">y</div>
Example (VBindStyle)

ExampleRender_vBindStyle shows :style with an object whose camelCase keys are automatically converted to kebab-case CSS property names.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	const src = `<template><p :style="{ color: 'red', fontSize: '14px' }">styled</p></template>`
	comp, err := htmlc.ParseFile("t.vue", src)
	if err != nil {
		log.Fatal(err)
	}
	out, err := htmlc.RenderString(comp, nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<p style="color:red;font-size:14px">styled</p>
Example (VFor)

ExampleRender_vFor shows v-for iterating over an array, with (item, index) destructuring, and over an integer range (n in N produces 1..N).

package main

import (
	"fmt"

	"github.com/dhamidi/htmlc"
)

func main() {
	// Plain array.
	c1, _ := htmlc.ParseFile("t.vue", `<template><li v-for="item in items">{{ item }}</li></template>`)
	o1, _ := htmlc.RenderString(c1, map[string]any{"items": []any{"a", "b", "c"}})
	fmt.Println(o1)

	// Array with zero-based index.
	c2, _ := htmlc.ParseFile("t.vue", `<template><li v-for="(item, i) in items">{{ i }}:{{ item }}</li></template>`)
	o2, _ := htmlc.RenderString(c2, map[string]any{"items": []any{"x", "y"}})
	fmt.Println(o2)

	// Integer range: n iterates 1, 2, 3.
	c3, _ := htmlc.ParseFile("t.vue", `<template><span v-for="n in 3">{{ n }}</span></template>`)
	o3, _ := htmlc.RenderString(c3, nil)
	fmt.Println(o3)
}
Output:
<li>a</li><li>b</li><li>c</li>
<li>0:x</li><li>1:y</li>
<span>1</span><span>2</span><span>3</span>
Example (VForObject)

ExampleRender_vForObject shows v-for iterating map entries using the (value, key) destructuring form.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	const src = `<template><dt v-for="(value, key) in obj">{{ key }}: {{ value }}</dt></template>`
	comp, err := htmlc.ParseFile("t.vue", src)
	if err != nil {
		log.Fatal(err)
	}
	// Use a single-entry map so iteration order is deterministic.
	out, err := htmlc.RenderString(comp, map[string]any{
		"obj": map[string]any{"lang": "Go"},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<dt>lang: Go</dt>
Example (VHtml)

ExampleRender_vHtml shows v-html="expr" which renders a raw HTML string as element content without escaping angle brackets or other HTML characters.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	const src = `<template><div v-html="raw"></div></template>`
	comp, err := htmlc.ParseFile("t.vue", src)
	if err != nil {
		log.Fatal(err)
	}
	out, err := htmlc.RenderString(comp, map[string]any{"raw": "<b>bold</b>"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<div><b>bold</b></div>
Example (VIf)

ExampleRender_vIf shows v-if/v-else-if/v-else conditional rendering: only the first truthy branch produces output; the rest are skipped entirely.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	const src = `<template><span v-if="score >= 90">A</span><span v-else-if="score >= 70">B</span><span v-else>C</span></template>`
	comp, err := htmlc.ParseFile("t.vue", src)
	if err != nil {
		log.Fatal(err)
	}
	out, err := htmlc.RenderString(comp, map[string]any{"score": float64(75)})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<span>B</span>
Example (VPre)

ExampleRender_vPre shows v-pre: mustache syntax inside the element is emitted literally without any interpolation or directive processing.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	const src = `<template><code v-pre>{{ raw }}</code></template>`
	comp, err := htmlc.ParseFile("t.vue", src)
	if err != nil {
		log.Fatal(err)
	}
	out, err := htmlc.RenderString(comp, map[string]any{"raw": "ignored"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<code>{{ raw }}</code>
Example (VShow)

ExampleRender_vShow shows v-show: a falsy expression injects style="display:none" while the element is still present in the DOM; a truthy expression renders the element normally.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	const src = `<template><p v-show="false">hidden</p><p v-show="true">visible</p></template>`
	comp, err := htmlc.ParseFile("t.vue", src)
	if err != nil {
		log.Fatal(err)
	}
	out, err := htmlc.RenderString(comp, nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<p style="display:none">hidden</p><p>visible</p>
Example (VText)

ExampleRender_vText shows v-text="expr" which sets element text content with HTML escaping, replacing any child nodes.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	const src = `<template><p v-text="msg"></p></template>`
	comp, err := htmlc.ParseFile("t.vue", src)
	if err != nil {
		log.Fatal(err)
	}
	out, err := htmlc.RenderString(comp, map[string]any{"msg": "Hello & World"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<p>Hello &amp; World</p>

func RenderString

func RenderString(c *Component, scope map[string]any) (string, error)

RenderString is a convenience wrapper that creates a temporary Renderer for c and renders it against scope, returning the result as a string. It does not collect styles or support component composition; use NewRenderer with WithStyles and WithComponents for those features.

func ScopeCSS

func ScopeCSS(css, scopeAttr string) string

ScopeCSS rewrites the CSS text so that every selector in every non-@-rule has scopeAttr appended to its last compound selector. scopeAttr should be a full attribute selector string, e.g. "[data-v-a1b2c3d4]".

@-rules (such as @media or @keyframes) are passed through verbatim, including their nested blocks.

func ScopeID

func ScopeID(path string) string

ScopeID computes the scope attribute name for a component at the given file path. The result is "data-v-" followed by the 8 lower-case hex digits of the FNV-1a 32-bit hash of path.

func SubstituteMissingProp

func SubstituteMissingProp(name string) (any, error)

SubstituteMissingProp returns a placeholder string "MISSING PROP: <name>" for any missing prop.

Types

type Component

type Component struct {
	// Template is the root of the parsed HTML node tree for the <template> section.
	Template *html.Node
	// Script is the raw text content of the <script> section (empty if absent).
	Script string
	// Style is the raw text content of the <style> section (empty if absent).
	Style string
	// Scoped reports whether the <style> tag carried the scoped attribute.
	Scoped bool
	// Path is the source file path passed to ParseFile.
	Path string
}

Component holds the parsed representation of a .vue Single File Component.

func ParseFile

func ParseFile(path, src string) (*Component, error)

ParseFile parses a .vue Single File Component from src and returns a Component. Only the top-level <template>, <script>, and <style> sections are extracted. <script> and <style> are optional; <template> is required. The template HTML is parsed into a node tree accessible via Component.Template.

Example

ExampleParseFile parses an inline .vue source string and inspects the resulting Component's path.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	comp, err := htmlc.ParseFile("Greeting.vue", `<template><p>Hello!</p></template>`)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(comp.Path)
}
Output:
Greeting.vue

func (*Component) Props

func (c *Component) Props() []PropInfo

Props walks the component's parsed template AST and returns all top-level variable references (props) that the template uses.

Identifiers starting with '$' are excluded. v-for loop variables are excluded within their subtree.

Example

ExampleComponent_Props shows how to call Props() to discover the prop names a component template uses and the expressions in which they appear.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	comp, err := htmlc.ParseFile("t.vue", `<template><p>{{ message }}</p></template>`)
	if err != nil {
		log.Fatal(err)
	}
	props := comp.Props()
	fmt.Println(len(props))
	fmt.Println(props[0].Name)
	fmt.Println(props[0].Expressions[0])
}
Output:
1
message
message

type Directive

type Directive interface {
	// Created is called before the element is rendered. The hook receives a
	// shallow-cloned working node whose Attr slice and Data field may be
	// freely mutated; mutations affect what the renderer emits for this
	// element but do not modify the shared parsed template.
	//
	// Common uses:
	//   - Add, remove, or rewrite attributes (node.Attr).
	//   - Change the element tag (node.Data) to redirect rendering to a
	//     different component (see VSwitch for an example).
	//   - Return a non-nil error to abort rendering of this element.
	Created(node *html.Node, binding DirectiveBinding, ctx DirectiveContext) error

	// Mounted is called after the element's closing tag has been written to w.
	// The hook may write additional HTML after the element.
	//
	// w is the same writer the renderer uses; bytes written here appear
	// immediately after the element in the output stream.
	//
	// Return a non-nil error to abort rendering.
	Mounted(w io.Writer, node *html.Node, binding DirectiveBinding, ctx DirectiveContext) error
}

Directive is the interface implemented by custom directive types.

Register a custom directive with Engine.RegisterDirective or Renderer.WithDirectives. In a template, reference it as v-<name>:

<div v-my-directive="someExpr">…</div>

Only the Created and Mounted hooks are called because htmlc renders server- side. There are no DOM updates, component unmounting, or browser events.

type DirectiveBinding

type DirectiveBinding struct {
	// Value is the result of evaluating the directive expression against the
	// current scope. For example, v-switch="item.type" yields item.type's value.
	Value any

	// RawExpr is the un-evaluated expression string from the template attribute.
	RawExpr string

	// Arg is the directive argument after the colon, e.g. "href" in v-bind:href.
	// Empty string when no argument is present.
	Arg string

	// Modifiers is the set of dot-separated modifiers, e.g. {"prevent": true}
	// from v-on:click.prevent. Empty map when no modifiers are present.
	Modifiers map[string]bool
}

DirectiveBinding holds the evaluated binding for a custom directive invocation.

type DirectiveContext

type DirectiveContext struct {
	// Registry is the component registry the renderer is using. Directives
	// like VSwitch use this to verify or resolve component names.
	Registry Registry
}

DirectiveContext provides directive hooks read-only access to renderer state.

type DirectiveRegistry

type DirectiveRegistry map[string]Directive

DirectiveRegistry maps directive names (without the "v-" prefix) to their implementations. Keys are lower-kebab-case; the renderer normalises names before lookup.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is the entry point for rendering .vue components. Create one with New; call RenderPage or RenderFragment to produce HTML. ServeComponent wraps a component as a net/http handler so it can be mounted directly in an http.Handler-based server.

Engine is safe for concurrent use. All render methods may be called from multiple goroutines simultaneously.

Example (ScopedStyles)

ExampleEngine_scopedStyles shows that a component with <style scoped> adds a unique data attribute to each HTML element and scopes its CSS selectors to match only elements within that component.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	// ParseFile with a fixed path gives a deterministic scope ID.
	const path = "Button.vue"
	comp, err := htmlc.ParseFile(path, `<template><p>hello</p></template><style scoped>p{color:red}</style>`)
	if err != nil {
		log.Fatal(err)
	}
	sc := &htmlc.StyleCollector{}
	out, err := htmlc.NewRenderer(comp).WithStyles(sc).RenderString(nil)
	if err != nil {
		log.Fatal(err)
	}
	items := sc.All()
	fmt.Println(out)
	fmt.Println(items[0].CSS)
}
Output:
<p data-v-6fc690bb>hello</p>
p[data-v-6fc690bb]{color:red}

func New

func New(opts Options) (*Engine, error)

New creates an Engine configured by opts. If opts.ComponentDir is set the directory is walked recursively and all *.vue files are registered.

func (*Engine) Components

func (e *Engine) Components() []string

Components returns the names of all registered components in sorted order. Lowercase aliases added automatically by the engine are excluded.

func (*Engine) Has

func (e *Engine) Has(name string) bool

Has reports whether name is a registered component.

func (*Engine) Mount

func (e *Engine) Mount(mux *http.ServeMux, routes map[string]string)

Mount registers a set of component routes on mux. The routes map keys are patterns accepted by http.ServeMux (e.g. "GET /{$}", "GET /about"), and values are component names. Each component is served as a full HTML page via ServePageComponent with no data function (use WithDataMiddleware to inject common data, or register routes manually for per-route data).

func (*Engine) Register

func (e *Engine) Register(name, path string) error

Register manually adds a component from path to the engine's registry under name, without requiring a directory scan. This is useful when components are generated programmatically or loaded from locations outside ComponentDir.

func (*Engine) RegisterDirective

func (e *Engine) RegisterDirective(name string, dir Directive)

RegisterDirective adds a custom directive to the engine under the given name (without the "v-" prefix). It replaces any previously registered directive with the same name. Panics if dir is nil.

func (*Engine) RegisterFunc

func (e *Engine) RegisterFunc(name string, fn func(...any) (any, error)) *Engine

RegisterFunc adds a per-engine function available in all template expressions rendered by this engine. The function can be called from templates as name(). Engine-level functions act as lower-priority builtins: the render data scope takes precedence over them, which in turn takes precedence over the global expr.RegisterBuiltin table.

RegisterFunc returns the Engine so calls can be chained.

func (*Engine) RenderFragment

func (e *Engine) RenderFragment(w io.Writer, name string, data map[string]any) error

RenderFragment renders name as an HTML fragment, writing the result to w, and prepends the collected <style> block to the output. Unlike RenderPage, it does not search for a </head> tag — it simply places the styles before the HTML, making it suitable for partial page updates (e.g. HTMX responses, turbo frames, or any context where a complete HTML document structure is not present).

For full HTML documents that include a <head> section, use RenderPage instead so that styles are injected in the document head.

func (*Engine) RenderFragmentContext

func (e *Engine) RenderFragmentContext(ctx context.Context, w io.Writer, name string, data map[string]any) error

RenderFragmentContext is like RenderFragment but accepts a context.Context. The render is aborted and ctx.Err() is returned if the context is cancelled or its deadline is exceeded during rendering.

func (*Engine) RenderFragmentString

func (e *Engine) RenderFragmentString(name string, data map[string]any) (string, error)

RenderFragmentString renders name as an HTML fragment and returns the result as a string. It is a convenience wrapper around RenderFragment for callers that need a string rather than writing to an io.Writer.

func (*Engine) RenderPage

func (e *Engine) RenderPage(w io.Writer, name string, data map[string]any) error

RenderPage renders name as a full HTML page, writing the result to w. It collects all scoped styles from the component tree and inserts them as a <style> block immediately before the first </head> tag, keeping styles in the document head where browsers expect them. If the output contains no </head> the style block is prepended to the output instead.

Use RenderPage when rendering a complete HTML document (e.g. a page component that includes <!DOCTYPE html>, <html>, <head>, and <body>). For partial HTML — such as HTMX responses or turbo-frame updates — use RenderFragment instead, which prepends styles without searching for </head>.

Example

ExampleEngine_RenderPage demonstrates full-page rendering: collected <style> blocks are injected immediately before </head>.

package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/dhamidi/htmlc"
)

func main() {
	dir, err := os.MkdirTemp("", "htmlc-example-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(dir)

	const vue = `<template><html><head><title>Demo</title></head><body><p>Hello</p></body></html></template><style>body{margin:0}</style>`
	if err := os.WriteFile(filepath.Join(dir, "Page.vue"), []byte(vue), 0644); err != nil {
		log.Fatal(err)
	}

	engine, err := htmlc.New(htmlc.Options{ComponentDir: dir})
	if err != nil {
		log.Fatal(err)
	}

	out, err := engine.RenderPageString("Page", nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<html><head><title>Demo</title><style>body{margin:0}</style></head><body><p>Hello</p></body></html>

func (*Engine) RenderPageContext

func (e *Engine) RenderPageContext(ctx context.Context, w io.Writer, name string, data map[string]any) error

RenderPageContext is like RenderPage but accepts a context.Context. The render is aborted and ctx.Err() is returned if the context is cancelled or its deadline is exceeded during rendering.

func (*Engine) RenderPageString

func (e *Engine) RenderPageString(name string, data map[string]any) (string, error)

RenderPageString renders name as a full HTML page and returns the result as a string. It is a convenience wrapper around RenderPage for callers that need a string rather than writing to an io.Writer.

func (*Engine) ServeComponent

func (e *Engine) ServeComponent(name string, data func(*http.Request) map[string]any) http.HandlerFunc

ServeComponent returns an http.HandlerFunc that renders name as a fragment and writes it with content-type "text/html; charset=utf-8". The data function is called on every request to obtain the data map passed to the template; it may be nil (in which case no data is provided).

Data middleware registered via WithDataMiddleware is applied after the data function, allowing common data (e.g. the current user or CSRF token) to be injected globally.

Example

ExampleEngine_ServeComponent shows how ServeComponent wraps a component as an http.HandlerFunc, demonstrated with an httptest round-trip.

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"

	"github.com/dhamidi/htmlc"
)

func main() {
	dir, err := os.MkdirTemp("", "htmlc-example-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(dir)

	if err := os.WriteFile(filepath.Join(dir, "Hello.vue"), []byte(`<template><p>hello</p></template>`), 0644); err != nil {
		log.Fatal(err)
	}

	engine, err := htmlc.New(htmlc.Options{ComponentDir: dir})
	if err != nil {
		log.Fatal(err)
	}

	h := engine.ServeComponent("Hello", nil)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rec := httptest.NewRecorder()
	h(rec, req)

	fmt.Println(rec.Code)
	fmt.Println(rec.Header().Get("Content-Type"))
	fmt.Println(rec.Body.String())
}
Output:
200
text/html; charset=utf-8
<p>hello</p>

func (*Engine) ServePageComponent

func (e *Engine) ServePageComponent(name string, data func(*http.Request) (map[string]any, int)) http.HandlerFunc

ServePageComponent returns an http.HandlerFunc that renders name as a full HTML page (using RenderPage, which injects styles into </head>) and writes it with content-type "text/html; charset=utf-8".

The data function is called on every request to obtain the data map and the HTTP status code to send. If the data function is nil, a 200 OK response with no template data is used. A status code of 0 is treated as 200.

Data middleware registered via WithDataMiddleware is applied after the data function.

func (*Engine) ValidateAll

func (e *Engine) ValidateAll() []ValidationError

ValidateAll checks every registered component for unresolvable child component references and returns a slice of ValidationError (one per problem). An empty slice means all components are valid.

ValidateAll is intended to be called once at application startup to surface missing-component problems early ("fail fast").

func (*Engine) WithDataMiddleware

func (e *Engine) WithDataMiddleware(fn func(*http.Request, map[string]any) map[string]any) *Engine

WithDataMiddleware adds a function that is called on every HTTP-triggered render to augment the data map. Middleware functions are called in registration order; later middleware can overwrite keys set by earlier ones.

WithDataMiddleware returns the Engine so calls can be chained.

func (*Engine) WithMissingPropHandler

func (e *Engine) WithMissingPropHandler(fn MissingPropFunc) *Engine

WithMissingPropHandler sets the function called when any component rendered by this engine has a missing prop. If not set, missing props cause render errors.

type MissingPropFunc

type MissingPropFunc func(name string) (any, error)

MissingPropFunc is called when a prop expected by the component's template is not present in the render scope. It receives the prop name and returns a substitute value, or an error to abort rendering.

type Options

type Options struct {
	// ComponentDir is the directory to scan recursively for *.vue files.
	// Components are discovered by walking the tree in lexical order; each file
	// is registered by its base name without extension (e.g. "Button.vue"
	// becomes "Button"). When two files share the same base name the last one
	// encountered in lexical-order traversal wins.
	ComponentDir string
	// Reload enables hot-reload for development use. When true, the engine
	// checks the modification time of every registered component file before
	// each render and automatically re-parses any file that has changed since
	// it was last loaded. This lets you edit .vue files and see the results
	// without restarting the server. For production, leave Reload false and
	// create the Engine once at startup.
	//
	// When FS is also set, reload only works if the FS implements fs.StatFS.
	// If it does not, reload is silently skipped for all entries.
	Reload bool
	// FS, when set, is used instead of the OS filesystem for all file reads
	// and directory walks. ComponentDir is then interpreted as a path within
	// this FS. This allows callers to use embedded filesystems (//go:embed),
	// in-memory virtual filesystems, or any other fs.FS implementation.
	//
	// When FS is nil, the OS filesystem is used (default behaviour).
	FS fs.FS
	// Directives registers custom directives available to all components rendered
	// by this engine. Keys are directive names without the "v-" prefix
	// (e.g. "switch" handles v-switch). Built-in directives (v-if, v-for, etc.)
	// cannot be overridden.
	Directives DirectiveRegistry
}

Options holds configuration for creating a new Engine.

type ParseError

type ParseError struct {
	// Path is the source file path.
	Path string
	// Msg is the human-readable description of the parse failure.
	Msg string
}

ParseError is returned when a .vue file cannot be parsed.

func (*ParseError) Error

func (e *ParseError) Error() string

type PropInfo

type PropInfo struct {
	Name        string
	Expressions []string
}

PropInfo describes a single top-level prop (variable reference) found in a component's template, together with the expression strings in which it appears.

type Registry

type Registry map[string]*Component

Registry maps component names to their parsed components. Keys may be PascalCase (e.g., "Card") or kebab-case (e.g., "my-card"). Registry is part of the low-level API; most callers should use Engine, which builds and maintains a Registry automatically from a component directory.

type RenderError

type RenderError struct {
	// Component is the component name being rendered when the error occurred.
	Component string
	// Expr is the template expression that triggered the error (may be empty).
	Expr string
	// Wrapped is the underlying error.
	Wrapped error
}

RenderError is returned when template rendering fails for a named component.

func (*RenderError) Error

func (e *RenderError) Error() string

func (*RenderError) Unwrap

func (e *RenderError) Unwrap() error

type Renderer

type Renderer struct {
	// contains filtered or unexported fields
}

Renderer walks a component's parsed template and produces HTML output. It is the low-level rendering primitive — most callers should use Engine (via RenderPage or RenderFragment) rather than constructing a Renderer directly. Use NewRenderer when you need fine-grained control over style collection or registry attachment.

func NewRenderer

func NewRenderer(c *Component) *Renderer

NewRenderer creates a Renderer for c. Call WithStyles and WithComponents before Render to enable style collection and component composition.

func (*Renderer) Render

func (r *Renderer) Render(w io.Writer, scope map[string]any) error

Render evaluates the component's template against the given data scope and writes the rendered HTML directly to w.

func (*Renderer) RenderString

func (r *Renderer) RenderString(scope map[string]any) (string, error)

RenderString evaluates the component's template against the given data scope and returns the rendered HTML as a string. It is a convenience wrapper around Render.

func (*Renderer) WithComponents

func (r *Renderer) WithComponents(reg Registry) *Renderer

WithComponents attaches a component registry to this renderer, enabling component composition. Returns the Renderer for chaining.

func (*Renderer) WithContext

func (r *Renderer) WithContext(ctx context.Context) *Renderer

WithContext attaches a context.Context to the renderer. The render is aborted and ctx.Err() is returned if the context is cancelled or its deadline is exceeded. Returns the Renderer for chaining.

func (*Renderer) WithDirectives

func (r *Renderer) WithDirectives(dr DirectiveRegistry) *Renderer

WithDirectives attaches a custom directive registry. Directives registered here are invoked when the renderer encounters v-<name> attributes that are not built-in directives. Returns the Renderer for chaining.

func (*Renderer) WithMissingPropHandler

func (r *Renderer) WithMissingPropHandler(fn MissingPropFunc) *Renderer

WithMissingPropHandler sets a handler that is called when a prop referenced in the template is not present in the render scope. Returns the Renderer for chaining.

Example

ExampleRenderer_WithMissingPropHandler shows SubstituteMissingProp substituting a placeholder string when a required template prop is absent.

package main

import (
	"fmt"
	"log"

	"github.com/dhamidi/htmlc"
)

func main() {
	comp, err := htmlc.ParseFile("t.vue", `<template><p>{{ name }}</p></template>`)
	if err != nil {
		log.Fatal(err)
	}
	out, err := htmlc.NewRenderer(comp).
		WithMissingPropHandler(htmlc.SubstituteMissingProp).
		RenderString(nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
<p>MISSING PROP: name</p>

func (*Renderer) WithStyles

func (r *Renderer) WithStyles(sc *StyleCollector) *Renderer

WithStyles sets sc as the StyleCollector that will receive this component's style contribution when Render is called. It returns the Renderer for chaining.

type SlotDefinition

type SlotDefinition struct {
	Nodes       []*html.Node
	ParentScope map[string]any
	BindingVar  string
	Bindings    []string
}

SlotDefinition captures everything needed to defer slot rendering: the AST nodes from the caller's template, the caller's scope at invocation time, and the parsed binding information from the v-slot / # directive.

type StyleCollector

type StyleCollector struct {
	// contains filtered or unexported fields
}

StyleCollector accumulates StyleContributions from one or more component renders into a single ordered list, deduplicating repeated contributions from the same scoped component. It is part of the low-level API; Engine creates and manages a StyleCollector automatically on each render call.

func (*StyleCollector) Add

func (sc *StyleCollector) Add(c StyleContribution)

Add appends c to the collector, skipping duplicates. Two contributions are considered duplicates when they share the same composite key (ScopeID + CSS), so the same scoped component rendered N times contributes its CSS only once, while different components or differing global CSS blocks are each kept.

func (*StyleCollector) All

func (sc *StyleCollector) All() []StyleContribution

All returns all StyleContributions in the order they were added. The slice is nil when no contributions have been collected.

type StyleContribution

type StyleContribution struct {
	// ScopeID is the scope attribute name (e.g. "data-v-a1b2c3d4") for a
	// scoped component's styles, or empty for global (non-scoped) styles.
	ScopeID string
	// CSS is the stylesheet text. For scoped components it has already been
	// rewritten by ScopeCSS; for global styles it is passed through verbatim.
	CSS string
}

StyleContribution holds a style block contributed by a component during render.

type VSwitch

type VSwitch struct{}

VSwitch is a built-in custom directive that replaces the host element with a registered component whose name is given by the directive's expression.

Usage in a template:

<div v-switch="item.type" :title="item.title" />

When rendered, the <div> tag is replaced by the component whose name matches the evaluated value of item.type (e.g. "CardWidget" or "card-widget"). Any other attributes on the host element (:title, class, etc.) are forwarded as props to the target component.

Registration:

engine.RegisterDirective("switch", &htmlc.VSwitch{})

VSwitch implements Directive via its Created hook; Mounted is a no-op.

func (*VSwitch) Created

func (v *VSwitch) Created(node *html.Node, binding DirectiveBinding, ctx DirectiveContext) error

Created changes the host element's tag to the component name supplied by the directive expression, and removes the v-switch attribute. After Created returns, the renderer sees a node whose Data is the component name and resolves it normally from the registry.

func (*VSwitch) Mounted

func (v *VSwitch) Mounted(_ io.Writer, _ *html.Node, _ DirectiveBinding, _ DirectiveContext) error

Mounted is a no-op for VSwitch.

type ValidationError

type ValidationError struct {
	// Component is the name of the component that has the problem.
	Component string
	// Message describes the problem.
	Message string
}

ValidationError describes a single problem found by Engine.ValidateAll.

func (ValidationError) Error

func (e ValidationError) Error() string

Directories

Path Synopsis
cmd
htmlc command
Package expr implements the expression language used in htmlc templates.
Package expr implements the expression language used in htmlc templates.
Package htmlctest provides helpers for testing htmlc components.
Package htmlctest provides helpers for testing htmlc components.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL