{"id":35743702,"url":"https://github.com/doors-dev/gox","last_synced_at":"2026-04-21T22:07:34.898Z","repository":{"id":328471222,"uuid":"1115226325","full_name":"doors-dev/gox","owner":"doors-dev","description":"Go language extension that turns HTML templates into typed Go expressions with seamless editor support and an extensible rendering pipeline.","archived":false,"fork":false,"pushed_at":"2026-04-20T12:53:02.000Z","size":762,"stargazers_count":50,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-20T13:40:01.292Z","etag":null,"topics":["go","html","jsx","lsp","templates"],"latest_commit_sha":null,"homepage":"https://doors.dev/docs/template-syntax","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/doors-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-12T14:26:45.000Z","updated_at":"2026-04-20T12:51:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/doors-dev/gox","commit_stats":null,"previous_names":["doors-dev/gox"],"tags_count":74,"template":false,"template_full_name":null,"purl":"pkg:github/doors-dev/gox","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doors-dev%2Fgox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doors-dev%2Fgox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doors-dev%2Fgox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doors-dev%2Fgox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/doors-dev","download_url":"https://codeload.github.com/doors-dev/gox/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doors-dev%2Fgox/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32112092,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-21T11:25:29.218Z","status":"ssl_error","status_checked_at":"2026-04-21T11:25:28.499Z","response_time":128,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","html","jsx","lsp","templates"],"created_at":"2026-01-06T16:15:41.197Z","updated_at":"2026-04-21T22:07:34.891Z","avatar_url":"https://github.com/doors-dev.png","language":"Go","funding_links":[],"categories":["Template Engines","Go"],"sub_categories":["HTTP Clients"],"readme":"![GoX](https://github.com/doors-dev/gox/raw/main/logo.png)\n\n# GoX — HTML templates as first-class Go expressions\n[![codecov](https://codecov.io/gh/doors-dev/gox/branch/main/graph/badge.svg?token=N5S703MVQ4)](https://codecov.io/gh/doors-dev/gox)\n[![Go Report Card](https://goreportcard.com/badge/github.com/doors-dev/gox)](https://goreportcard.com/report/github.com/doors-dev/gox)\n[![Go Reference](https://pkg.go.dev/badge/github.com/doors-dev/gox.svg)](https://pkg.go.dev/github.com/doors-dev/gox)\n[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)\n\nGoX lets you write HTML templates as typed Go expressions that compile to plain Go on the *go*.\n\n- **Seamless editor support:** near-native language server experience across `.gox` and `.go` files.\n- **Full templating toolbox:** conditionals, loops, composition, and reusable components.\n- **Extensible rendering pipeline:** templates compile to a stream of render jobs, processable with custom printers.\n- **`templ` compatible:** `gox.Elem` implements `Render(ctx, w)` and drops in wherever a `templ` component is expected.\n\n\n\u003e Syntax guide: [doors.dev/docs/template-syntax](https://doors.dev/docs/template-syntax)  \n\n\u003e For practical extensions on top of GoX, see [`github.com/doors-dev/goxx`](https://github.com/doors-dev/goxx).\n\n\u003e This README focuses on installation, workflow, editor integration, and the rendering API behind GoX.\n\n---\n\n## Install\n\n### Install the `gox` tool\n\n\u003e Not required, [VS Code](https://github.com/doors-dev/vscode-gox) or [Neovim](https://github.com/doors-dev/nvim-gox) extensions is enough to get started.\n\nThe easiest path is the prebuilt binary from [GitHub Releases](https://github.com/doors-dev/gox/releases).\n\nTo install from source:\n\n```bash\nmake install\n```\n\nThat builds the bundled Rust formatter and installs `gox`. Building from source requires Go, Cargo, and a working native toolchain.\n\n### Editor integration\n\nRecommended: use the official [VS Code](https://github.com/doors-dev/vscode-gox) or [Neovim](https://github.com/doors-dev/nvim-gox) extension.\n\nThe bare `gox` command starts the GoX language server. `gox srv` is the explicit form of the same command.\n\nIf you are wiring an editor manually:\n\n1. Run `gox` or `gox srv`.\n2. Attach it to both `.go` and `.gox` buffers.\n3. Disable a separate workspace `gopls` client. GoX launches and proxies a `gopls` instance.\n4. Make sure `gopls` is on `PATH`, or pass `-gopls /path/to/gopls`.\n5. Install the [tree-sitter-gox](https://github.com/doors-dev/tree-sitter-gox) grammar.\n\nUseful server flags:\n\n- `-gopls` to point at a specific `gopls` binary\n- `-listen` to expose the server over TCP or a Unix socket instead of stdio\n- `-listen.timeout` to stop an idle socket server after a timeout\n\nGoX sits in front of `gopls`. It parses `.gox`, generates `.x.go`, keeps source and target positions mapped both ways, and forwards normal Go language features through a `gopls` instance.\n\n### Add the Go package\n\n```bash\ngo get github.com/doors-dev/gox\n```\n\nKeep the installed `gox` tool and the Go module version reasonably in sync. Generated `.x.go` files carry a GoX version marker, and newer generated files require a tooling upgrade.\n\n## How to Render an `Elem`\n\nMost of the time, rendering starts with `Elem.Render(ctx, w)`.\n\n```go\nfunc Badge(label string) gox.Elem {\n    return \u003cspan class=\"badge\"\u003e~(label)\u003c/span\u003e\n}\n\nfunc handleBadge(w http.ResponseWriter, r *http.Request) {\n    w.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\")\n    if err := Badge(\"New\").Render(r.Context(), w); err != nil {\n        http.Error(w, err.Error(), http.StatusInternalServerError)\n    }\n}\n```\n\nOutside HTTP, you can render to any `io.Writer`:\n\n```go\nfunc Example() error {\n    return Badge(\"New\").Render(context.Background(), os.Stdout)\n}\n```\n\nIf you need the HTML as a string, render into a buffer:\n\n```go\nfunc RenderBadge(label string) (string, error) {\n    var buf bytes.Buffer\n    err := Badge(label).Render(context.Background(), \u0026buf)\n    return buf.String(), err\n}\n```\n\nUse `Elem.Print(ctx, printer)` instead when you want to render through a custom `Printer` instead of the default HTML writer.\n\n---\n\n## Workflow\n\n### File layout\n\nA typical package can contain all three kinds of files:\n\n```text\nmain.go     # regular Go\npage.gox    # source template\npage.x.go   # generated Go\n```\n\nWhat you edit:\n\n- edit `.gox`\n- treat `.x.go` as generated output\n- keep normal `.go` files alongside both\n\nWhat the tooling does:\n\n- the language server regenerates `.x.go` on save\n- `gox gen` does the same in batch mode\n- orphaned `.x.go` files are removed automatically\n\nRules worth following:\n\n- do not edit `.x.go` manually\n- do not use the `.x.go` suffix for hand-written files\n- if a generated file was produced by a newer GoX version, upgrade the tooling before continuing\n\n### Commands you actually use\n\n```text\ngox                 # start the language server\ngox gen             # generate .x.go files for the current directory\ngox gen ./pkg       # generate a specific file or directory\ngox fmt             # format .gox and .go files in the current directory\ngox fmt ./internal  # format a specific directory\ngox fmt ./main.go   # format a specific file\ngox ver             # print the GoX version\n```\n\nBy default, `gox gen` and `gox fmt` use the current directory and respect `.gitignore`. Both commands also accept an optional positional file or directory path. `-no-ignore` disables ignore handling, and `-force` skips target-file safety checks during generation.\n\n### What happens under the hood\n\nThe `.gox` parser produces a syntax tree. The assembler walks that tree and lowers template nodes into plain Go built around `gox.Elem(func(cur gox.Cursor) error { ... })`.\n\nAlongside generation, GoX keeps a source-to-target translation map between `.gox` and `.x.go`. That mapping is used for the language server features.\n\n`gox fmt` formats `.gox` with the bundled Rust formatter and regular `.go` files with `gofmt`. Embedded `\u003cscript\u003e` and `\u003cstyle\u003e` blocks are reformatted too.\n\n---\n\n## Rendering Model\n\nGenerated `.gox` code ultimately works with four things:\n\n- render values such as `Elem`, `Comp`, and templ-compatible components\n- a `Cursor` that builds structure and emits rendering operations\n- an `Attrs` set attached to an open head before it is submitted and exposed via cursor methods\n- a stream of `Job` values consumed by a `Printer`\n\nThat is the useful mental model for GoX. Once you understand those pieces, generated `.x.go` files are easy to read and the advanced hooks stop feeling magical.\n\n### Primitives\n\nThese are the core renderable types:\n\n```go\ntype Comp interface {\n    Main() Elem\n}\n\ntype Elem func(cur Cursor) error\n```\n\n`Elem` is the main render value in GoX. It is a function that renders through a `Cursor`.\n\nIt also:\n\n- implements `Comp` by returning itself from `Main()`\n- renders directly with `Render(ctx, w)`\n- renders through custom pipelines with `Print(ctx, printer)`\n\nGoX also defines a minimal `Templ` interface for values that render with `Render(ctx, w)`, and `Cursor.Any` knows how to emit those too.\n\nSo in normal Go code, a template is just a value you can return, store, pass around, and render.\n\n### `Cursor`\n\n`Cursor` is the low-level rendering state machine. It streams operations to a `Printer` and tracks active element heads so it can enforce correct ordering.\n\nThere are three head lifecycles:\n\n1. Regular element\n   `Init(tag)` -\u003e optional `Set` / `Modify` -\u003e `Submit()` -\u003e child content -\u003e `Close()`\n2. Void element\n   `InitVoid(tag)` -\u003e optional `Set` / `Modify` -\u003e `Submit()`\n3. Container\n   `InitContainer()` -\u003e child content -\u003e `Close()`\n\nRegular and void heads become HTML tags. Containers do not emit a tag, but they still create open and close jobs in the stream, which makes them useful for grouping and for render-time transformations.\n\nThe important state rule is:\n\n- before `Submit()`, you are still building a head and may mutate attributes\n- after `Submit()`, you may emit child content, but you may no longer mutate that head\n\n`cur.Context()` returns the default context for jobs emitted through that cursor. `cur.Send()` forwards a prebuilt job directly to the underlying printer and bypasses cursor state validation.\n\nGenerated `.x.go` files are mostly straightforward cursor code, similar to:\n\n```go\nfunc Badge(label string) gox.Elem {\n    return gox.Elem(func(cur gox.Cursor) error {\n        if err := cur.Init(\"span\"); err != nil {\n            return err\n        }\n        if err := cur.Set(\"class\", \"badge\"); err != nil {\n            return err\n        }\n        if err := cur.Submit(); err != nil {\n            return err\n        }\n        if err := cur.Text(label); err != nil {\n            return err\n        }\n        return cur.Close()\n    })\n}\n```\n\nMost placeholder rendering ends up calling `Cursor.Any` or `Cursor.Many`.\n\n`Cursor.Any` understands:\n\n- `string` and `[]string`\n- `Elem` and `[]Elem`\n- `Comp` and `[]Comp`\n- `Job` and `[]Job`\n- `Editor`\n- `Templ`\n- `[]interface{}`\n\nAnything else falls back to escaped `fmt.Fprint`.\n\n### Attributes\n\n`Attrs` is the mutable attribute set attached to a head while that head is being built.\n\nYou mainly encounter attributes in three places:\n\n- when generated code or hand-written cursor code calls `Set`\n- when code calls `Modify` to attach one or more render-time modifiers\n- when custom printers or proxies inspect `JobHeadOpen.Attrs`\n\n`AttrSet` and `AttrMod` remain as deprecated compatibility aliases for `Set`\nand `Modify`.\n\nImportant details from the API:\n\n- attributes are stored sorted by name\n- names are case-sensitive\n- `nil` means \"unset\"\n- `false` means \"unset\"\n- any other non-nil value means \"set\"\n\nThe attribute system is also an extension point:\n\n```go\ntype Modify interface {\n    Modify(ctx context.Context, tag string, attrs Attrs) error\n}\n\ntype Mutate interface {\n    Mutate(name string, value any) any\n}\n\ntype Output interface {\n    Output(w io.Writer) error\n}\n```\n\n`Modify` is head-level, not value-level. It runs right before a head is rendered, receives the full `Attrs` set, and can inspect or change the final attributes for that element. This is the hook used by render-time attribute transformations.\n\n`Mutate` is value-level. It lets a new attribute value depend on the previous value already stored under the same name.\n\n`Output` lets a value control its own escaped output.\n\nThat makes attributes more than plain HTML metadata. They are also part of the rendering pipeline.\n\n### Extension points\n\nGoX exposes three main render-time extension points:\n\n- `Editor` for code that needs direct cursor access\n- `Proxy` for wrapping or rebasing an element subtree before it renders\n- `Printer` for consuming and transforming the emitted job stream\n\n#### `Editor`\n\n`Editor` is the escape hatch for render-time behavior that needs direct cursor access.\n\nUse it when rendering needs to:\n\n- emit low-level jobs manually\n- work directly with `cur.Context()`\n- integrate with a larger rendering runtime\n- do something more specific than \"return another subtree\"\n\n#### `Proxy`\n\nA `Proxy` wraps an `Elem` subtree before it renders.\n\nYou can do any type of transofrmation with proxy, for example:\n\n- change attributes\n- convert tags\n- render the subtree through a custom printer before forwarding it\n- basically any transormation\n\nA common implementation pattern is a proxy printer: call `elem.Print(cur.Context(), customPrinter)`, inspect the first `*gox.JobHeadOpen` or `*gox.JobComp`, adjust it, then forward the rest into the current cursor.\n\n#### `Printer`\n\nRendering is a job stream.\n\nUseful concrete job types include:\n\n- `*gox.JobHeadOpen`\n- `*gox.JobHeadClose`\n- `*gox.JobText`\n- `*gox.JobRaw`\n- `*gox.JobBytes`\n- `*gox.JobComp`\n- `*gox.JobTempl`\n- `*gox.JobFprint`\n- `*gox.JobError`\n\nImportant behavior from the API:\n\n- open and close jobs for the same head share an `ID`\n- container head jobs emit no HTML, but still exist in the stream\n- the default printer from `gox.NewPrinter` checks `j.Context().Err()` before calling `Output`\n- jobs are pooled and single-use\n\nCustom printers are where GoX opens up the most. They can buffer, transform, route, inspect, or reinterpret the stream instead of just writing HTML sequentially.\n\n### Helper adapters\n\nThe helpers in `helpers.go` keep the API lightweight when you want one-off implementations:\n\n- `gox.EditorComp` for values that should be both `Editor` and `Comp`\n- `gox.EditorCompFunc`\n- `gox.EditorFunc`\n- `gox.ProxyFunc`\n- `gox.ModifyFunc`\n- `gox.PrinterFunc`\n- `gox.NewEscapedWriter`\n\n`gox.NewEscapedWriter` is useful when custom rendering code needs the same escaping rules as GoX text and attribute output.\n\n---\n\n\u003e *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.*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoors-dev%2Fgox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdoors-dev%2Fgox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoors-dev%2Fgox/lists"}