Hacker News new | ask | show | jobs
Show HN: Go language extension with HTML templates (github.com)
3 points by derstruct 63 days ago
I created a Go language extension that turns HTML templates into typed Go expressions and adds `elem` primitives. Works via own language server by proxying gopls with extra features on top + runtime package.

In core ideas it's sumular to `templ` - it compiles to Go, has own extension (`.gox`), language server, CLI tool and IDE plugins, can write to `io.Writer`. But there are much more differences.

1. HTML supported as a first-class Go expression. For example this is valid syntax: `hello := <h1>Hello World!</h1>`. Also `elem` keyword is acting like `templ`, but supports anonymous functions:

``` var f = elem(text string) { <h1>~(text)</h1> } ```

2. I aimed for superior LSP experience, my language server architecture is different, what allowed be to enable LSP edits (rename works!) and support seamless LSP navigation (go to definition, calls, etc - won't point you to generated file). Also it manages generated files automatically (compiles as you type and writes as you save, no need to run `generate` command manually)

3. Improved parsing stability. I based parser on extended Go tree-sitter grammar + made syntax more distinctive. In practice, this means better developer experience during edits and less artifacts (it's not triggered by keywords in a plain text)

4. Different primitives set. I have `gox.Elem` which is a concrete type and `gox.Comp` interface with `Main() gox.Elem` function. That allowed to extend feature set on basic `gox.Elem` while enable you to write components without lowering to `Render(ctx, io.Writer)` interface.

5. Extensible rendering pipeline. In `GoX` HTML is converted to stream of typed jobs, that can be preprocessed in any way. For example you can add your own element `<eb>` and on render convert it to `<span class="font-extrabold">`.

Also it has various extensibility points, like you can read and alter attributes as regular go values. There is even special interface `gox.Modify`, that can read and set/unset any attributes when rendered: `<svg (styles.NormSVG) ...>` can set `fill`, `stoke` and unset existing width/height.

I basically tried to solve all issues I had with templ while shifting general design towards better extensibility with minimal core and I think I succeed.

In terms of raw performance, templ will beat `GoX` every time. Extra layer I added is still an extra layer. It's microsecond scales, can't imagine it as a bottleneck, but anyway.

P.S. I build it primarily to support my server-driven web app runtime/framework, but it works standalone perfectly and is templ-compatible.

2 comments

Actually with tree-sitter it's much less moving parts inside parser-generator core (5-10x LOC less then templ, but it's difficult to compare because structure differences).

My theory that I don't have to care a lot about gopls changes, because I rely on LSP protocol itself when doing conversion. I basically scan exchange JSONs for locations (sometimes recursively) that are related to `.gox` or generated `.x.go` and perform targeted patches.

About string literals completions.. I can't imagine how I could achieve even a 1/10 of feature set with this approach. Or at the end, it will turn out as something very similar to what I have, but achieved from a different starting point.

Interesting that you went the full custom-language route instead of just better gopls completion inside string literals. The tree-sitter grammar plus language server proxying to gopls is a lot of moving parts to maintain. Curious how you handle gopls version drift, since it changes behavior pretty often.
Accidentally replied in a different comment, first time.