Skip to content
/ gox Public

Go language extension that turns HTML templates into typed Go expressions with seamless editor support and an extensible rendering pipeline.

License

Notifications You must be signed in to change notification settings

doors-dev/gox

Repository files navigation

GoX

GoX — HTML templates as first-class Go expressions

GoX is a Go language extension that lets you write HTML-like templates as typed expressions that compile to plain Go.

func Hello(name: string) gox.Elem {
    return <h1>
        Hello ~(name)!
    </h1>
}

// or

elem Hello(name: string) {
    <h1>
        Hello ~(name)!
    </h1>
}
  • Seamless editor support: near-native language-server experience across .gox and regular .go files.
  • Full templating toolbox: conditionals, loops, composition, and reusable components.
  • Extensible rendering pipeline: templates compile to a stream of render jobs you can process with custom printers.
  • templ compatible: gox.Elem renders via Render(ctx, w) and can be used where a templ component is expected.

How To Install

Set up your development environment

Recommended: use the official VS Code or Neovim extension.

The editor extension is enough to get started. If you want the CLI tool or prefer a manual setup:

  1. Download the gox binary from GitHub Releases and add it to your PATH (or build it from source).
  2. Install the Tree-sitter grammar: https://github.com/doors-dev/tree-sitter-gox

Configuration

The editor extensions configure everything automatically. See their documentation for available options.

For manual setup:

  1. Attach the GoX language server to both .go and .gox buffers (see gox srv -help for available options).
  2. Disable the default Go language server (gopls). GoX starts its own gopls instance and proxies features while adding .gox support.
  3. Ensure gopls is on your PATH, or configure a custom path for gox.

Add the dependency to your project

go get github.com/doors-dev/gox

Keep the tooling version and Go package version in sync. Currently, the language server does not enforce this automatically.


How To Use

Extended syntax is available in .gox files.

The GoX language server compiles .gox files into generated .x.go files and keeps them up to date whenever you save a .gox file.

  • If an existing .x.go file was generated by a newer version of GoX, the server will report an error until you upgrade.
  • Orphaned .x.go files are deleted automatically. Avoid naming regular files with the .x.go suffix.
  • Do not edit .x.go files manually.

You can also trigger generation manually using the CLI:

gox gen [flags]

  -force
        overwrite existing files without checking
  -no-ignore
        ignore .gitignore
  -path string
        file or directory to generate (default ".")

Core Concepts

Elem

gox.Elem is the fundamental renderable type:

  • Conceptually: a function that emits output through a cursor (type Elem func(cur Cursor) error)
  • Practical: a value you can return, pass around, compose, and render.

Elem also:

  • implements gox.Comp (so an element can be used as a component)
  • renders through Render(ctx, w) for templ compatibility
  • can be rendered through Print(ctx, printer) for custom pipelines

Cursor → Jobs → Printer

At runtime, GoX renders by streaming jobs into a printer:

  • A Job is a unit of work: it carries a context.Context and an Output(w) method.
  • A Printer consumes jobs. The default printer writes output sequentially to an io.Writer, and stops early if job.Context().Err() is non-nil (canceled/deadline exceeded).

This architecture enables custom printers for preprocessing, analysis, instrumentation, or alternative rendering strategies.


Syntax

1. Element

An element is an HTML tag expression:

el := <div></div>

You can return it, pass it as a function argument, store it in a struct field, etc.

func Page(content gox.Elem) gox.Elem {
    return <html>
        <body>
            ~(content)
        </body>
    </html>
}

// call
Page(<h1>Hello!</h1>)

You can also use an anonymous wrapper tag (fragment) to group content without adding an actual HTML element: <>Hello</>

elem function shorthand

elem is a shorthand for functions that return gox.Elem:

elem Page(content gox.Elem) {
    <html>
        <body>
            ~(content)
        </body>
    </html>
}

Anonymous functions are also supported:

page := elem(content gox.Elem) {
    <html>
        <body>
            ~(content)
        </body>
    </html>
}

…and methods:

type Component struct {
    content gox.Elem
}

elem (c *Component) Main() {
    <html>
        <body>
            ~(c.content)
        </body>
    </html>
}
  • ctx is available inside templates and provides a context.Context.
  • Self-closing syntax works for any tag. For non-void tags, GoX will automatically emit a closing tag (<div/><div></div>).

2. Component

A component is anything that implements gox.Comp:

type Comp interface {
    Main() gox.Elem
}

For compatibility, gox.Elem also implements gox.Comp.


3. Placeholder

Insert values using a placeholder: ~(value):

<h1>Hello ~(name)!</h1>

You can also insert multiple values:

<h1>Hello ~(name, " ", surname)!</h1>

Values are rendered using default formatting, with special handling for:

  • gox.Comp / gox.Elem
  • templ.Component (from github.com/a-h/templ)
  • string, []string
  • []gox.Comp, []gox.Elem, []any (rendered item-by-item)
  • gox.Job, []gox.Job
  • gox.Editor

For simple literals you can omit the parentheses. Strings, numbers, composite literals (struct/array/slice/map):

~// render user card component
~UserCard{
    Id: id,
}

Advanced placeholder types

gox.Job writes directly to the output stream:

type Job interface {
    Context() context.Context
    Output(w io.Writer) error
}

gox.Editor provides low-level access to the rendering cursor:

type Editor interface {
    Edit(cur Cursor) error
}

4. Conditions and loops

If / else-if / else are available as expressions:

<div>
    ~(if user != nil {
        Hello ~(user.name)!
    } else if loggedOut {
        Bye!
    } else {
        Please log in
    })
</div>

Loops:

~(for _, user := range users {
    <tr>
        <td>~(user.name)</td>
        <td>~(user.email)</td>
    </tr>
})

5. Attributes

Values

Use parentheses to provide a Go expression as an attribute value:

elem block(id: string, content: any) {
    <div id=(id)>~(content)</div>
}
  • Any value is accepted.
  • If the value is false or nil, the attribute is omitted.
  • Attribute names are case-sensitive (class and Class are different attributes). Always use consistent casing.

Advanced attribute behavior

During rendering, the default formatter is used unless the value implements gox.Output:

type Output interface {
    Output(w io.Writer) error
}

To compute a new attribute value from the previous one, implement gox.Mutate:

type Mutate interface {
    Mutate(prev any) (new any)
}

To inspect or modify all attributes of an element right before it’s rendered, implement gox.AttrMod:

type AttrMod interface {
    Modify(ctx context.Context, tag string, attrs Attrs) error
}

Attribute modifiers are applied at render time and can mutate the full attribute set. To attach one, place it in parentheses inside the opening tag:

<button (LandingAction)>
    Request Demo!
</button>

You can attach multiple modifiers as a comma-separated list:

<button (TrackClick, LandingAction)>
    Request Demo!
</button>

6. Inline expression

An inline expression is a function literal evaluated immediately during rendering. Its return value is inserted exactly where it appears (node content or attribute value).

<div>
    ~func {
        user, err := Users.get(id)
        if err != nil {
            return <span>DB error</span>
        }
        return <div>~(Card(user))</div>
    }
</div>

In attributes:

<input type="checkbox" checked=func {
    user := Users.get(id)
    return user.Agreed // false or nil omits the attribute
}>

7. Go code, raw blocks, and comments

Go code

To run Go code during rendering (without rendering output), use ~{ ... }:

~{
    user := Users.Get(id)
}
<div>~(user.name)</div>

Comment

To comment inside templates, use ~// or ~/* ... */:

~// <div></div> - commented out

HTML comments are also supported.

Raw block tag

To output HTML verbatim (without escaping or template processing), wrap it in the special raw tag: <:>...</:>.

<:>
    <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
        <path d="..." />
    </svg>
</:>

Recommended for large static fragments—especially inline SVG—to reduce rendering overhead.


8. Proxies

A proxy captures an element subtree and can transform it before rendering. Apply a proxy by prefixing the target expression with ~>:

~>(proxy) <div>
    Proxy can apply transformations to this HTML.
</div>

A proxy must implement:

type Proxy interface {
    Proxy(cur Cursor, elem Elem) error
}

You can apply multiple proxies as a comma-separated list:

~>(Track, Minify, Instrument) <div>
    Proxy can apply transformations to this HTML.
</div>

Proxies can reduce boilerplate, add integrations, or collect analytics by wrapping/replacing subtree rendering.


Rendering

Default rendering

gox.Elem can be rendered directly to an io.Writer:

err := elem.Render(ctx, w)

Custom rendering pipelines

To preprocess output, analyze it, or build custom backends, implement a printer:

type Printer interface {
    Send(j Job) error
}

Jobs produced by GoX are pooled for performance. The default job types release themselves back to an internal pool after Output(...) completes.

Working with concrete job types

Send receives values through the gox.Job interface, but the jobs produced by GoX have concrete types (e.g. *JobHeadOpen, *JobHeadClose, *JobText, *JobRaw, *JobBytes, *JobComp, *JobTempl, *JobFprint, *JobError).

A custom printer can type-switch on the incoming job value to implement transforms, instrumentation, buffering, routing, or alternative output strategies:

func (p *MyPrinter) Send(j gox.Job) error {
    switch v := j.(type) {
    case *gox.JobHeadOpen:
        // inspect v.Tag / v.Kind / v.Attrs, record metrics, rewrite attrs, etc.
    case *gox.JobText:
        // observe/transform text
    case *gox.JobComp:
        // Component boundary:
        // - trace/measure per component, or
        // - render the component with this printer to intercept its internal jobs:
        //     if el := v.Comp.Main(); el != nil { return el.Print(v.Ctx, p) }
        // - or render it elsewhere (buffer/worker) and merge output in order.
    }
    return j.Output(p.w) // or route to another sink
}

Notes:

  • Treat incoming job objects as single-use: do not store them, do not reuse them, and do not keep references to their fields beyond the scope of Send.
  • The default NewPrinter implementation checks j.Context().Err() before calling j.Output(...). Custom printers should apply their own cancellation policy if needed.
  • The open and close jobs for the same head share the same ID (useful for pairing and tracing).
  • Container heads produce no HTML output, but still emit open/close jobs.
  • Element “open” and “close” jobs for the same head share the same ID.
  • “Container” heads exist for grouping in the job stream and produce no HTML output, but still have open/close jobs.

Disclaimer: GoX is an independent, third-party project and is not affiliated with, endorsed by, or sponsored by The Go Project, Google, or any official Go tooling.

About

Go language extension that turns HTML templates into typed Go expressions with seamless editor support and an extensible rendering pipeline.

Topics

Resources

License

Stars

Watchers

Forks