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.
[htmlc logo]
[htmlc logo]: ./logo.svg
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-switch="expr" (on <template>)
Switch-statement conditional rendering. Evaluates the expression once;
the first child with a matching v-case is rendered; v-default renders
when no case matched. Not part of stable Vue.js; implements RFC #482.
Example: <template v-switch="user.role">
<Admin v-case="'admin'" />
<User v-default />
</template>
v-case="expr"
Child of <template v-switch>. Rendered when its expression equals the
parent switch value (Go == comparison).
v-default
Child of <template v-switch>. Rendered when no preceding v-case matched.
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-bind="obj"
Attribute spreading: each key-value pair in the map becomes an HTML
attribute on the element. Keys "class" and "style" are merged with
any static and dynamic class/style attributes. Boolean attribute
semantics apply per key. On child components, the map is spread into
the component's prop scope (explicit :prop bindings take precedence
over spread values).
Example: <div v-bind="attrs"></div>
<Button v-bind="buttonProps" :type="'submit'" />
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>
Scope propagation ¶
Every component renders in an isolated scope that contains only the props explicitly passed to it as attributes. Parent scope variables are not inherited; this makes data flow visible and prevents accidental coupling.
Engine-level functions (registered with Engine.RegisterFunc) are the one exception: they are injected into every component's scope at every depth. Use RegisterFunc for helper functions — URL builders, route helpers, formatters — that need to be callable from any component without explicit prop threading.
WithDataMiddleware values are injected into the top-level render scope only. If a child component needs a middleware-supplied value, pass it down as an explicit prop.
// Good: helper functions via RegisterFunc — available everywhere
engine.RegisterFunc("url", buildURL)
engine.RegisterFunc("routeActive", checkActive)
// Good: per-request data via explicit props
// In Page.vue: <Shell :currentUser="currentUser" />
// In Shell.vue: {{ currentUser.Name }}
// Avoid: relying on middleware values inside child components
// — they are not automatically propagated.
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 custom directives via Options.Directives or Engine.RegisterDirective:
engine, err := htmlc.New(htmlc.Options{
ComponentDir: "templates/",
Directives: htmlc.DirectiveRegistry{
"my-dir": &MyDirective{},
},
})
The built-in VHighlight directive is the canonical example: it sets the background colour of the host element to the CSS colour string supplied by the directive's expression — mirroring the v-highlight example from the Vue.js custom directives guide. VHighlight is not pre-registered; to use it, add it via Options.Directives:
engine, err := htmlc.New(htmlc.Options{
ComponentDir: "templates/",
Directives: htmlc.DirectiveRegistry{
"highlight": &htmlc.VHighlight{},
},
})
DirectiveWithContent ¶
A directive that wants to replace the element's children with custom HTML may implement the optional DirectiveWithContent interface in addition to Directive. After Created is called the renderer checks whether the directive implements DirectiveWithContent and, if InnerHTML() returns a non-empty string, writes it verbatim between the opening and closing tags instead of rendering the template children.
External Directives ¶
htmlc build discovers external directives automatically from the component tree. Any executable file whose base name (without extension) matches v-<name> (lower-kebab-case) is registered as an external directive under that name. The executable communicates with htmlc over newline-delimited JSON on stdin/stdout, receiving a request envelope for each hook invocation and responding with a result envelope. See the README for the full protocol description.
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 ¶
- Variables
- func ErrorOnMissingProp(name string) (any, error)
- func Render(w io.Writer, c *Component, scope map[string]any) error
- func RenderString(c *Component, scope map[string]any) (string, error)
- func ScopeCSS(css, scopeAttr string) string
- func ScopeID(path string) string
- func SubstituteMissingProp(name string) (any, error)
- type Component
- type Directive
- type DirectiveBinding
- type DirectiveContext
- type DirectiveRegistry
- type DirectiveWithContent
- type Engine
- func (e *Engine) Components() []string
- func (e *Engine) Has(name string) bool
- func (e *Engine) Mount(mux *http.ServeMux, routes map[string]string)
- func (e *Engine) Register(name, path string) error
- func (e *Engine) RegisterDirective(name string, dir Directive)
- func (e *Engine) RegisterFunc(name string, fn func(...any) (any, error)) *Engine
- func (e *Engine) RenderFragment(w io.Writer, name string, data map[string]any) error
- func (e *Engine) RenderFragmentContext(ctx context.Context, w io.Writer, name string, data map[string]any) error
- func (e *Engine) RenderFragmentString(name string, data map[string]any) (string, error)
- func (e *Engine) RenderPage(w io.Writer, name string, data map[string]any) error
- func (e *Engine) RenderPageContext(ctx context.Context, w io.Writer, name string, data map[string]any) error
- func (e *Engine) RenderPageString(name string, data map[string]any) (string, error)
- func (e *Engine) ServeComponent(name string, data func(*http.Request) map[string]any) http.HandlerFunc
- func (e *Engine) ServePageComponent(name string, data func(*http.Request) (map[string]any, int)) http.HandlerFunc
- func (e *Engine) ValidateAll() []ValidationError
- func (e *Engine) WithDataMiddleware(fn func(*http.Request, map[string]any) map[string]any) *Engine
- func (e *Engine) WithMissingPropHandler(fn MissingPropFunc) *Engine
- type MissingPropFunc
- type Options
- type ParseError
- type PropInfo
- type Registry
- type RenderError
- type Renderer
- func (r *Renderer) Render(w io.Writer, scope map[string]any) error
- func (r *Renderer) RenderString(scope map[string]any) (string, error)
- func (r *Renderer) WithComponents(reg Registry) *Renderer
- func (r *Renderer) WithContext(ctx context.Context) *Renderer
- func (r *Renderer) WithDirectives(dr DirectiveRegistry) *Renderer
- func (r *Renderer) WithFuncs(funcs map[string]any) *Renderer
- func (r *Renderer) WithMissingPropHandler(fn MissingPropFunc) *Renderer
- func (r *Renderer) WithStyles(sc *StyleCollector) *Renderer
- type SlotDefinition
- type SourceLocation
- type StyleCollector
- type StyleContribution
- type VHighlight
- type ValidationError
Examples ¶
- Package
- Component.Props
- Engine (ScopedStyles)
- Engine.RenderPage
- Engine.ServeComponent
- ParseFile
- Render (ComponentProps)
- Render (ComponentSlot)
- Render (EventPassthrough)
- Render (Interpolation)
- Render (NamedSlots)
- Render (ScopedSlots)
- Render (SingleVariableSlotBinding)
- Render (SlotFallbackContent)
- Render (VBind)
- Render (VBindClass)
- Render (VBindStyle)
- Render (VFor)
- Render (VForObject)
- Render (VHtml)
- Render (VIf)
- Render (VPre)
- Render (VShow)
- Render (VText)
- Renderer.WithMissingPropHandler
Constants ¶
This section is empty.
Variables ¶
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 ErrorOnMissingProp ¶ added in v0.2.0
ErrorOnMissingProp is a MissingPropFunc that aborts rendering with an error whenever a prop is missing. Use it to restore strict validation:
renderer.WithMissingPropHandler(htmlc.ErrorOnMissingProp)
func Render ¶
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 & World</p>
func RenderString ¶
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 ¶
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 ¶
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 ¶
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
// Source is the raw source text of the file, stored for location-aware error reporting.
Source string
// Warnings holds non-fatal issues found during parsing, such as self-closing
// custom component tags that were automatically rewritten.
Warnings []string
}
Component holds the parsed representation of a .vue Single File Component.
func ParseFile ¶
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 ¶
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.
// - 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
// can use this to verify or resolve component names.
Registry Registry
}
DirectiveContext provides directive hooks read-only access to renderer state.
type DirectiveRegistry ¶
DirectiveRegistry maps directive names (without the "v-" prefix) to their implementations. Keys are lower-kebab-case; the renderer normalises names before lookup.
type DirectiveWithContent ¶ added in v0.5.0
type DirectiveWithContent interface {
Directive
// InnerHTML returns the raw HTML to use as the element's inner content.
// Return ("", false) to fall back to normal child rendering.
InnerHTML() (html string, ok bool)
}
DirectiveWithContent is an optional extension of the Directive interface. When a directive's Created hook wants to replace the element's children with custom HTML it should implement this interface.
The renderer checks for this interface after calling Created. If InnerHTML returns a non-empty string the element's template children are skipped and the string is written verbatim between the opening and closing tags (equivalent to v-html on the element itself).
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 ¶
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 ¶
Components returns the names of all registered components in sorted order. Lowercase aliases added automatically by the engine are excluded.
func (*Engine) Mount ¶
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 ¶
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 ¶
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 ¶
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.
Functions registered here are propagated automatically into every child component's scope, so they are available at any nesting depth without being threaded through as explicit props.
RegisterFunc returns the Engine so calls can be chained.
func (*Engine) RenderFragment ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
Data middleware applies only to the HTTP-triggered top-level render and is not automatically propagated into child component scopes. If child components need access to values injected by middleware, those values must be passed as explicit props or the same values should be registered via RegisterFunc.
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. The default behaviour (when no handler is set) is to render a visible "[missing: <name>]" placeholder in place of the prop value. Use ErrorOnMissingProp to restore strict error behaviour, or SubstituteMissingProp to use the legacy "MISSING PROP: <name>" format.
type MissingPropFunc ¶
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
// Debug enables debug render mode. When true, the rendered HTML is
// annotated with HTML comments describing component boundaries, expression
// values, conditional branch outcomes, and slot contents.
// Intended for development use only; never enable in production.
Debug bool
}
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
// Location holds the source position of the error, or nil if unknown.
Location *SourceLocation
}
ParseError is returned when a .vue file cannot be parsed.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
type PropInfo ¶
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 ¶
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
// Location holds the source position of the error, or nil if unknown.
Location *SourceLocation
}
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.
Engine-level functions (from Engine.RegisterFunc) are available in child components only when the renderer is created by Engine — Engine calls WithFuncs automatically. Callers using the low-level NewRenderer API who want engine functions to propagate into child components must call WithFuncs explicitly.
func NewRenderer ¶
NewRenderer creates a Renderer for c. Call WithStyles and WithComponents before Render to enable style collection and component composition. Call WithFuncs to make engine-registered functions available in this component and all child components rendered from it.
func (*Renderer) Render ¶
Render evaluates the component's template against the given data scope and writes the rendered HTML directly to w.
func (*Renderer) RenderString ¶
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 ¶
WithComponents attaches a component registry to this renderer, enabling component composition. Returns the Renderer for chaining.
func (*Renderer) WithContext ¶
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) WithFuncs ¶ added in v0.2.2
WithFuncs attaches engine-registered functions to this renderer so they are available in template expressions and propagated to all child renderers. 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
// Component is the component that authored this slot content.
// It is used to stamp the correct scope attribute on slot elements.
// May be nil when the parent has no component context (rare).
Component *Component
// SlotDefs holds the slot definitions that were active in the authoring
// component at the time this slot content was captured. When the slot
// content itself contains <slot /> elements they must be resolved against
// this map, not against the consuming component's slot definitions.
SlotDefs map[string]*SlotDefinition
}
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 SourceLocation ¶ added in v0.2.0
type SourceLocation struct {
File string // source file path
Line int // 1-based line number (0 = unknown)
Column int // 1-based column (0 = unknown)
Snippet string // ≈3-line context around the error (may be empty)
}
SourceLocation describes a position within a source file.
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 VHighlight ¶ added in v0.3.0
type VHighlight struct{}
VHighlight is an example custom directive that sets the background colour of the host element. It is the htmlc equivalent of the v-highlight directive shown in the Vue.js custom directives guide (https://vuejs.org/guide/reusability/custom-directives.html).
Register it on an engine and then use v-highlight in templates:
engine.RegisterDirective("highlight", &htmlc.VHighlight{})
Template usage:
<p v-highlight="'yellow'">Highlight this text bright yellow</p>
The expression must evaluate to a non-empty CSS colour string. If the host element already has a style attribute, the background property is appended; existing style declarations are preserved.
func (*VHighlight) Created ¶ added in v0.3.0
func (v *VHighlight) Created(node *html.Node, binding DirectiveBinding, ctx DirectiveContext) error
Created merges `background: <colour>` into the host element's style attribute.
func (*VHighlight) Mounted ¶ added in v0.3.0
func (v *VHighlight) Mounted(_ io.Writer, _ *html.Node, _ DirectiveBinding, _ DirectiveContext) error
Mounted is a no-op for VHighlight.
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
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
htmlc
command
|
|
|
v-syntax-highlight
module
|
|
|
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. |