Hey everyone! I've been lurking the past months watching the new compiler development. Exciting stuff! Now that things are coming together I'm looking to pick up the Jay again, the static site generator platform I worked on for the previous version of the compiler.
I've been thinking a bit about how to design the Html-builder API with the new Roc syntax, was wondering if anyone else has thoughts on that.
Here's one idea I have for an API, this is assuming strings can be interpreted as custom types at compile time (which I think is planned, right?):
page : Html
page =
"body"
.child("h1".text("Header text!"))
.child("a".href("./other-page.html").text("click me!"))
Any other designs already out there?
I thought you'd have to name the custom type like "h1".Tag.text("Header text!")
If I don't misunderstand the explicit type is to force interpretation as a certain type, but if I understand Richard correctly here it's not necessary:
so this is very interesting timing because I've been thinking about giving [ ... ] the same treatment as what we've done for number and string literals
more specifically, in the same way that you can now define your own nominal type with a from_numeral : ... method, and that method will get called at compile time if a number literal unifies with your type, and the same for from_quote : ... and string literals, I was thinking about having bracket literals have from_brackets : ...
I was about to send you a DM @Richard Feldman asking if that was possible... the usecase I had in mind was our unicode package for grapheme segmentation. Here is a snippet from the current impl -- which from memory I thought was a list pattern match, but it appears it's a tuple
# Used internally to implement the [UNICODE TEXT SEGMENTATION](https://www.unicode.org/reports/tr29/)
# algorithm.
split_help : _, List CodePoint, List GBP, Tokens -> Tokens
split_help = |state, code_points, break_points, acc|
next_c_ps = List.drop_first(code_points, 1)
next_b_ps = List.drop_first(break_points, 1)
when (state, code_points, break_points) is
# Special handling for empty list
(Next, [], _) -> acc
# Special handling for last codepoint
(Next, [cp], _) -> List.concat(acc, [CP(cp), BR(GB2)])
(AfterHangulL(prev), [cp], [bp]) if bp == L or bp == V or bp == LV or bp == LVT -> List.concat(acc, [CP(prev), NB(GB6), CP(cp), BR(GB2)])
(AfterHangulLVorV(prev), [cp], [bp]) if bp == V or bp == T -> List.concat(acc, [CP(prev), NB(GB7), CP(cp), BR(GB2)])
(AfterHangulLVTorT(prev), [cp], [bp]) if bp == T -> List.concat(acc, [CP(prev), NB(GB8), CP(cp), BR(GB2)])
(AfterHangulL(prev), [_], [_]) -> split_help(LastWithPrev(prev), code_points, break_points, acc)
(AfterHangulLVorV(prev), [_], [_]) -> split_help(LastWithPrev(prev), code_points, break_points, acc)
(AfterHangulLVTorT(prev), [_], [_]) -> split_help(LastWithPrev(prev), code_points, break_points, acc)
(LastWithPrev(prev), [cp], [bp]) if bp == Control or bp == CR or bp == LF -> List.concat(acc, [CP(prev), BR(GB5), CP(cp), BR(GB2)])
(LastWithPrev(prev), [cp], [bp]) if bp == Extend -> List.concat(acc, [CP(prev), NB(GB9), CP(cp), BR(GB2)])
(LastWithPrev(prev), [cp], [bp]) if bp == ZWJ ->
if prev |> CodePoint.to_u32 |> InternalEmoji.is_pictographic then
List.concat(acc, [CP(prev), NB(GB11), CP(cp), BR(GB2)])
else
List.concat(acc, [CP(prev), NB(GB9), CP(cp), BR(GB2)])
yeah we could do the same thing for tuples if desired
I'm also not saying the following is a good idea, just exploring, but we've also talked about a subscript operator - e.g. foo[bar] desugars to foo.subscript(bar) - we could combine these in an arguably silly way:
page : Html
page =
body[[
h1[["Header text!"]],
a[["click me!"]].href("./other-page.html")
]]
so that would be where you'd implement subscript and have it accept a collection of child elements etc.
(I don't think a "varargs subscript" would work, e.g. a[b, c, d] desugars to a.subscript(b, c, d) because we don't have a concept of "function that accepts varargs" and I don't think adding one would be a good idea)
I think a .subscript method like that could have all manner of uses, good and ill. If I understand it correctly, it could be used a bit like a callable trait, to turn any nominal type into something function-like. You might use it to do something constructor-like for instance: Person[{ name: "Fred", age: 54 }].
This got me thinking, suppose you'd build the Html API around a different operator:
page : Html
page =
body + [
h1 + ["Header text!"],
a.href("./other-page.html") + ["click me!"],
]
I think this has the same main downside as (ab)using .subscript: being maybe a bit too cute. But otherwise I think I might like this API better than the subscript-based one (which isn't to say I love it necessarily), because:
+ instead of [..] makes it more intuitively clear that we're overloading an operator for cutesy API-design. The [[ .. ]] looks too much like dedicated syntax for my taste.+ makes more sense to me than [..] for the act of constructing a thing.Upside of the original design I posted compared to both operator-based designs: Using separate .child and .text methods for adding different types of Html nodes frees .from_str for creating tags ("hi"). That in turn means you don't need to qualify or expose a bunch of functions from Html.
I like the original method chaining / builder API.
I've realized the operator-based approach could be made to work with string-based tags using a different operator for text and non-text nodes, like this:
page : Html
page =
"body"
+ ("h1" * "Header text!")
+ ("a".href("./other-page.html") * "click me!")
This currently doesn't compile, because the compiler doesn't realize "a" isn't a Str here, but I think that might be a bug?
unfortunately that's not a bug, it's a limitation
if you look at the expression "a".href("./other-page.html") in isolation
the only clue the compiler has about whether or not "a" is a Str is the return type of href
but it can't know what href is a method on :smile:
so basically the type of "a".href("./other-page.html" is
val where [
val.from_quote : _ -> _,
val.href : _ -> _,
]
and even if the return type of href is statically known, that still doesn't give the compiler any clues about what val is - it could be anything with an href and a from_quote method, as long as the return type lined up!
so it defaults to Str and then says "hey Str doesn't have href"
I don't think the string literal idea necessarily works for this DSL unfortunately :sweat_smile:
Ah, got it. I guess it's for the better, the whole thing was a bit magical anyhow.
For anyone interested, this is where I'm at currently with this design:
import Html exposing [html, attr]
page : Html
page =
html.body(
attr,
html.h1(attr, "Header text!")
.a(attr.href("./other-page.html"), "click me!"),
)
For context: I think for Jay Html behind the scenes is best represented as a stream of tokens. This because most of the Html is produced by the host when it reads source files, and the platform will for the most part embed this content from the platform in some wrapper structures. I'm hoping I'll be able to stream most of this Html through the host without too much allocating.
Builder-style API's that construct an Html tag with something like Html.a.href("link.html").text("click!") are tricky if Html is represented as a stream behind the scenes, for a couple of reasons:
Html.Builder type that has an explicit .finish() method or something to go from Html.Builder to Html, but I would prefer not to need that.The above factors pushed me to a design where a single function call produces an entire node similar to what Elm does. I quite like the result by itself though:
attr to nodes without attributes. That I'd like to tidy up a bit more if possible.from_quote for convenient text-node creation.Jasper Woudenberg said:
[…] needing to pass
attrto nodes without attributes
Just so I understand, what precisely is attr, technically, in this design? The default (empty) instance of an AttributeSet type or whatever it's named, which can be populated using the fluent builder chain?
Does it depend in any way on the “element”, like .h1(), it got placed in?
Lukas Juhrich said:
Just so I understand, what precisely is
attr, technically, in this design? The default (empty) instance of anAttributeSettype or whatever it's named, which can be populated using the fluent builder chain?
Yep, that's it exactly, an empty attribute-set!
There's no relation to the element it's placed in, so currently it's allowed to use any attribute with any html element.
I'm not sure if this is a good idea, but something you could explore is doing the attributes in the same builder style - so like you can do .a().href(...)
Richard Feldman said:
I'm not sure if this is a good idea, but something you could explore is doing the attributes in the same builder style - so like you can do
.a().href(...)
I think that would either require a structural Html implementation (rather then the tokenstream I'm using now), because each call of a builder method would need to modify a tag-under-construction.
not sure about the tradeoffs but fwiw the way GPUI does this is the other way around: nested elements get added with .child(...) or .children(...) and then attributes are builder-style modifying the element after the fact
Last updated: Jul 23 2026 at 13:15 UTC