Update kjol website with C documentation

This commit is contained in:
2026-07-14 13:05:12 -04:00
parent 02a6dc6c48
commit 7d7b7354df
66 changed files with 23884 additions and 2551 deletions

View File

@@ -8,6 +8,7 @@ import (
chart "github.com/wcharczuk/go-chart/v2"
. "kjol/vdom"
"kjol/wasmruntime"
ui "kjol/webui"
)
@@ -60,9 +61,56 @@ func pieSVG(values []int) string {
})
}
// chartSkeleton is what the SERVER puts where a chart is going to be: a box of the right
// height, so nothing jumps when the real one arrives.
func chartSkeleton(height string) *VNode {
return Div(Attr("class", "flex animate-pulse items-center justify-center rounded-default bg-surface-muted "+height),
Span(Attr("class", "text-xs text-ink-faint"), Text("drawing…")),
)
}
// newChartDrawing returns a signal that is FALSE on the server and on the client's first
// render, and true from the moment the WebAssembly has committed that first render.
//
// It is what keeps the charts CLIENT-DRAWN. go-chart is ordinary Go and would run just as
// happily on the server — it used to, and this page's markup carried two finished SVGs.
// Two reasons not to:
//
// - It is work the server does on every single request for a picture that only matters
// once the page is alive. Drawing it in the browser costs the server nothing and the
// reader nothing they can see.
// - It is the more honest demonstration. A Go charting library, compiled to WebAssembly,
// drawing an SVG in the browser is the thing this layer claims it can do. Shipping a
// server-rendered picture of one proves the opposite point.
//
// The false-on-first-render part is not optional: hydration walks the server's DOM
// alongside the client's first tree, so that tree has to be the SAME tree. Draw the charts
// on the client's first pass and the two disagree, and the reconciler has to rebuild what
// it should have adopted.
func newChartDrawing() *Signal[bool] {
drawn := NewSignal(false)
wasmruntime.AfterRender(func() {
if !drawn.Get() {
drawn.Set(true) // a write re-renders; the second pass draws for real
}
})
return drawn
}
// chartBox renders one chart, or the placeholder standing in for it. draw is a closure so
// that on the server go-chart is never called at all — not called and discarded, but never
// entered.
func chartBox(class, height string, drawn bool, draw func() string) *VNode {
if !drawn {
return Div(Attr("class", class), chartSkeleton(height))
}
return Div(Attr("class", class), Raw(draw()))
}
//gowasm:page /wasm/chart static layout=app
func ChartPage(d Deps) func() *VNode {
data := NewSignal(fixedChartData())
drawn := newChartDrawing()
return func() *VNode {
values := data.Get()
@@ -85,19 +133,27 @@ func ChartPage(d Deps) func() *VNode {
),
docSection("charts", "A worked example: charts",
prose("These charts are SVG produced by go-chart — a plain Go library that knows nothing about "+
"the browser. The server draws them and ships the markup inline; there is no chart "+
"JavaScript, and no canvas that has to wait for the client to boot before it shows anything."),
prose("Shuffle re-runs the same drawing code in the browser. The first render came from the "+
"server and the next one comes from WebAssembly, and the page cannot tell the difference."),
prose("These charts are SVG produced by go-chart — a plain Go library that knows nothing "+
"about the browser. They are drawn by the WEBASSEMBLY, in your browser, and never by the "+
"server: what the server sends is the two placeholders you may have seen for a moment, "+
"and the WebAssembly replaces them on its first commit."),
prose("That is the demonstration. A Go charting library, compiled to wasm, drawing an SVG in "+
"the browser is exactly what this layer claims it can do — and a server-rendered picture "+
"of a chart would prove the opposite point while looking identical. Shuffle redraws them, "+
"and no request is made."),
prose("The rest of the page IS server-rendered — the headings, the prose, the code you are "+
"reading. Static and client-drawn are not opposites: a route can be pre-rendered and still "+
"leave the expensive, browser-only parts of itself for the client."),
Div(Attr("class", "mt-4"),
ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Icon: "chart-column", Text: "Shuffle data",
OnClick: func() { data.Set(randomValues()) }}),
),
Div(Attr("class", "mt-4 grid gap-4 lg:grid-cols-12"),
Div(Attr("class", "lg:col-span-7 rounded-default border border-line bg-surface p-3 shadow-xs overflow-auto"), Raw(barSVG(values))),
Div(Attr("class", "lg:col-span-5 rounded-default border border-line bg-surface p-3 shadow-xs overflow-auto"), Raw(pieSVG(values))),
chartBox("lg:col-span-7 rounded-default border border-line bg-surface p-3 shadow-xs overflow-auto",
"h-[260px]", drawn.Get(), func() string { return barSVG(values) }),
chartBox("lg:col-span-5 rounded-default border border-line bg-surface p-3 shadow-xs overflow-auto",
"h-[320px]", drawn.Get(), func() string { return pieSVG(values) }),
),
note("go-chart lives in the EXAMPLE, not in kjol",
@@ -111,7 +167,7 @@ func ChartPage(d Deps) func() *VNode {
apiRow{"//gowasm:page /path static", "Pre-render this route on the server, then hydrate it."},
apiRow{"vdom.RenderHTML", "Render a tree to an HTML string. This is what the server calls."},
apiRow{"wasmruntime.Hydrate", "Adopt server-rendered DOM instead of building it. The client's entry point for a static route."},
apiRow{"vdom.Raw", "Insert markup verbatim — how the server-drawn SVG gets in. The reconciler clears it correctly when the element is reused."},
apiRow{"vdom.Raw", "Insert markup verbatim — how the SVG the WebAssembly drew gets in. The reconciler clears it correctly when the element is reused."},
),
),
)
@@ -120,17 +176,42 @@ func ChartPage(d Deps) func() *VNode {
const chartSnippet = `//gowasm:page /wasm/chart static layout=app
func ChartPage(d Deps) func() *VNode {
data := NewSignal(fixedChartData())
data := NewSignal(fixedChartData())
drawn := NewSignal(false) // false on the server AND on the first client render
// AfterRender is the post-commit hook. It fires once the WebAssembly has put its
// first tree on the page — the earliest moment at which drawing is a client act.
wasmruntime.AfterRender(func() {
if !drawn.Get() {
drawn.Set(true) // a write re-renders; the second pass draws
}
})
return func() *VNode {
// go-chart draws an SVG string — on the server for the first paint,
// and in the browser for every render after that.
return Div(
ui.Button(ui.ButtonProps{
Text: "Shuffle data",
OnClick: func() { data.Set(randomValues()) },
}),
Div(Raw(barSVG(data.Get()))),
// The server never enters barSVG: chartBox takes a CLOSURE, and calls it only
// once drawn is true. It renders the placeholder instead, and the WebAssembly
// swaps in the real chart on its first commit.
//
// drawn must be FALSE on the client's first render too. Hydration walks the
// server's DOM alongside the client's first tree, so the two have to BE the
// same tree; draw on that first pass and the reconciler rebuilds what it
// should have adopted.
chartBox("", "h-[260px]", drawn.Get(),
func() string { return barSVG(data.Get()) }),
)
}
}
// chartBox is the whole trick, and it is four lines.
func chartBox(class, height string, drawn bool, draw func() string) *VNode {
if !drawn {
return Div(Attr("class", class), chartSkeleton(height))
}
return Div(Attr("class", class), Raw(draw()))
}`