diff --git a/cmd/sync/sync.go b/cmd/sync/sync.go index 7ac29fd..b209101 100644 --- a/cmd/sync/sync.go +++ b/cmd/sync/sync.go @@ -447,17 +447,54 @@ func (r *linkRewriter) RewriteRelative(body string, page PageEntry, ref string) if inFence { continue } - lines[i] = markdownLinkRE.ReplaceAllStringFunc(ln, func(m string) string { - target := markdownLinkRE.FindStringSubmatch(m)[1] - if !isRelativeRef(target) { - return m - } - return "](" + r.resolveRelative(target, repo, srcDir, ref) + ")" + lines[i] = rewriteOutsideCodeSpans(ln, func(seg string) string { + return markdownLinkRE.ReplaceAllStringFunc(seg, func(m string) string { + target := markdownLinkRE.FindStringSubmatch(m)[1] + if !isRelativeRef(target) { + return m + } + return "](" + r.resolveRelative(target, repo, srcDir, ref) + ")" + }) }) } return strings.Join(lines, "\n") } +// rewriteOutsideCodeSpans applies f to the parts of a line that are NOT inside +// a backtick-delimited inline code span. +// +// Fenced blocks were already skipped, but inline spans were not, and Go generic +// call syntax inside one — `AssertPureState[T](t)` — contains the exact +// `](...)` shape a markdown link does. Rewriting it turned documented code into +// a URL in five places across the reference and guides before this was caught. +// +// Splitting on ` gives alternating outside/inside segments: even indices are +// prose, odd indices are code. +// +// An ODD number of backticks means the spans cannot be paired, and the parity +// is then wrong from the stray one onward — code would land on an "outside" +// index and be rewritten, which is the corruption this exists to prevent. Such +// a line is left entirely alone. The cost is a genuine link on a malformed line +// going un-rewritten, which leaves it exactly as upstream wrote it; the +// alternative is mangling documented code, and only one of those is recoverable +// by a later fix. +func rewriteOutsideCodeSpans(line string, f func(string) string) string { + n := strings.Count(line, "`") + if n == 0 { + return f(line) + } + if n%2 != 0 { + return line + } + parts := strings.Split(line, "`") + for i := range parts { + if i%2 == 0 { + parts[i] = f(parts[i]) + } + } + return strings.Join(parts, "`") +} + // resolveRelative maps one upstream-relative link target to its docs-site or // GitHub destination, preserving any #fragment. func (r *linkRewriter) resolveRelative(target, repo, srcDir, ref string) string { diff --git a/cmd/sync/sync_test.go b/cmd/sync/sync_test.go index f69f4d4..e60e208 100644 --- a/cmd/sync/sync_test.go +++ b/cmd/sync/sync_test.go @@ -531,3 +531,52 @@ func TestRewriteRelative_BareUnmappedBecomesGitHubURL(t *testing.T) { t.Errorf("bare unmapped sibling not rewritten to GitHub: %q", got) } } + +func TestRewriteRelative_LeavesInlineCodeSpansAlone(t *testing.T) { + // Go generic call syntax inside a code span contains the same ](...) + // shape a markdown link does. Rewriting it corrupted documented code in + // five places across the reference and guides before this was caught. + r := newLinkRewriter(relCfg()) + page := PageEntry{SiteURL: "/reference/session", SourceRepo: "https://github.com/livetemplate/livetemplate", SourcePath: "docs/references/session.md"} + + body := "Use `AssertPureState[T](t)` in tests, and see [the API](api-reference.md)." + got := r.RewriteRelative(body, page, "v0.23.0") + + if !strings.Contains(got, "`AssertPureState[T](t)`") { + t.Errorf("inline code span was rewritten: %q", got) + } + if !strings.Contains(got, "[the API](/reference/api)") { + t.Errorf("real link outside the span should still rewrite: %q", got) + } +} + +func TestRewriteRelative_MultipleCodeSpansOnOneLine(t *testing.T) { + r := newLinkRewriter(relCfg()) + page := PageEntry{SiteURL: "/reference/session", SourceRepo: "https://github.com/livetemplate/livetemplate", SourcePath: "docs/references/session.md"} + + body := "`f[A](t)` then [x](api-reference.md) then `g[B](t)` then [y](../guides/SCALING.md)" + got := r.RewriteRelative(body, page, "v0.23.0") + + for _, keep := range []string{"`f[A](t)`", "`g[B](t)`"} { + if !strings.Contains(got, keep) { + t.Errorf("code span %s was rewritten: %q", keep, got) + } + } + if !strings.Contains(got, "[x](/reference/api)") || !strings.Contains(got, "[y](/guides/scaling)") { + t.Errorf("links between spans should rewrite: %q", got) + } +} + +func TestRewriteRelative_UnbalancedBacktickErrsTowardNotRewriting(t *testing.T) { + // A stray backtick should not cause a corrupting rewrite; leaving the + // remainder alone is the safe failure direction. + r := newLinkRewriter(relCfg()) + page := PageEntry{SiteURL: "/reference/session", SourceRepo: "https://github.com/livetemplate/livetemplate", SourcePath: "docs/references/session.md"} + + body := "a stray ` backtick then `AssertPureState[T](t)`" + got := r.RewriteRelative(body, page, "v0.23.0") + + if strings.Contains(got, "github.com") { + t.Errorf("unbalanced backtick produced a rewrite: %q", got) + } +} diff --git a/content/changelog/livetemplate.md b/content/changelog/livetemplate.md index cec0d2d..45585bf 100644 --- a/content/changelog/livetemplate.md +++ b/content/changelog/livetemplate.md @@ -2,8 +2,8 @@ title: "Changelog" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "CHANGELOG.md" -source_ref: "v0.22.0" -source_commit: "22a4853506a682583b511e470bdd1e6193f4d5fe" +source_ref: "v0.23.0" +source_commit: "8294ce439a46a6a1f92e2a77b8a4978c9e526cc6" --- # Changelog @@ -13,6 +13,35 @@ All notable changes to LiveTemplate will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.23.0] - 2026-08-02 + +### Added + +- **`LiveHandler.Func()` returns `ServeHTTP` as an `http.HandlerFunc`.** The + value `Template.Handle()` returns already satisfied `http.Handler`, so + `http.Handle`/`mux.Handle` worked, but the stdlib entry points that take a + function — `http.HandleFunc`, and `ServeMux.HandleFunc` with Go 1.22 method + patterns — required spelling out `handler.ServeHTTP`. `Func()` is that method + value, so `http.HandleFunc("/counter", handler.Func())` and + `mux.HandleFunc("GET /counter", handler.Func())` read naturally. It is an + accessor, not a downgrade: `Shutdown`, `Publish` and `MetricsHandler` stay + available on the `LiveHandler` it came from. + +### Changed + +- **A failed WebSocket upgrade now logs a `hint` when the `http.ResponseWriter` + does not implement `http.Hijacker`.** An upgrade takes over the raw + connection, so middleware that wraps the writer (logging, gzip, status + capture) without forwarding `Hijack` breaks it — while GET and POST keep + rendering, making the symptom "the page renders but never goes live". The + underlying upgrader error names `http.Hijacker` but not the middleware that + caused it; the hint does, and points at forwarding `Hijack` or leaving the + writer unwrapped when `livetemplate.WSIsUpgrade(r)` is true. It is attached on + the writer's own defect, which need not be what the accompanying error reports + — an upgrader can reject a handshake earlier (a disallowed `Origin`) and never + reach the hijack — so it is worded as a second failure the upgrade would have + hit regardless, rather than as the reported cause. + ## [v0.22.0] - 2026-07-26 ### Added diff --git a/content/contributing/livetemplate.md b/content/contributing/livetemplate.md index 31f4642..25fbd03 100644 --- a/content/contributing/livetemplate.md +++ b/content/contributing/livetemplate.md @@ -2,8 +2,8 @@ title: "Contributing to LiveTemplate Core Library" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "CONTRIBUTING.md" -source_ref: "v0.22.0" -source_commit: "22a4853506a682583b511e470bdd1e6193f4d5fe" +source_ref: "v0.23.0" +source_commit: "8294ce439a46a6a1f92e2a77b8a4978c9e526cc6" --- # Contributing to LiveTemplate Core Library @@ -226,7 +226,7 @@ livetemplate/ └── scripts/ # Development scripts ``` -For the complete file-by-file map with line counts and dependencies, see [docs/design/CODE_STRUCTURE.md](https://github.com/livetemplate/livetemplate/blob/v0.22.0/docs/design/CODE_STRUCTURE.md). +For the complete file-by-file map with line counts and dependencies, see [docs/design/CODE_STRUCTURE.md](https://github.com/livetemplate/livetemplate/blob/v0.23.0/docs/design/CODE_STRUCTURE.md). **Note:** The client library, CLI tool, and examples are now in separate repositories: - Client: https://github.com/livetemplate/client @@ -528,7 +528,7 @@ Look for issues labeled `good first issue` - these are: ### Learning the Codebase 1. **Start with the Contributor Walkthrough** - - [`docs/guides/new-contributor-walkthrough.md`](https://github.com/livetemplate/livetemplate/blob/v0.22.0/docs/guides/new-contributor-walkthrough.md) - **START HERE!** Comprehensive guide to the 5-phase architecture with links to all code and tests + - [`docs/guides/new-contributor-walkthrough.md`](https://github.com/livetemplate/livetemplate/blob/v0.23.0/docs/guides/new-contributor-walkthrough.md) - **START HERE!** Comprehensive guide to the 5-phase architecture with links to all code and tests 2. **Read the architecture docs** - `CLAUDE.md` - Development guidelines diff --git a/content/guides/ephemeral-components.md b/content/guides/ephemeral-components.md index ce34da4..0d0594b 100644 --- a/content/guides/ephemeral-components.md +++ b/content/guides/ephemeral-components.md @@ -2,8 +2,8 @@ title: "Ephemeral Components Guide" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/guides/ephemeral-components.md" -source_ref: "v0.22.0" -source_commit: "22a4853506a682583b511e470bdd1e6193f4d5fe" +source_ref: "v0.23.0" +source_commit: "8294ce439a46a6a1f92e2a77b8a4978c9e526cc6" --- # Ephemeral Components Guide @@ -62,7 +62,7 @@ type AppState struct { } ``` -> **Note on `AssertPureState`**: If your tests use `lvt/testing.AssertPureState[T](https://github.com/livetemplate/livetemplate/blob/v0.22.0/docs/guides/t)` to verify state contains no dependency types, `*toast.Container` will need to be excluded. Component containers are not external dependencies — they hold transient UI data, not connections or handles. Use `AssertPureState` with the `IgnoreFields` option, or structure your state so component fields live in a separate struct that is not checked. +> **Note on `AssertPureState`**: If your tests use `lvt/testing.AssertPureState[T](t)` to verify state contains no dependency types, `*toast.Container` will need to be excluded. Component containers are not external dependencies — they hold transient UI data, not connections or handles. Use `AssertPureState` with the `IgnoreFields` option, or structure your state so component fields live in a separate struct that is not checked. ### Initialization diff --git a/content/guides/observability.md b/content/guides/observability.md index 7dbec02..b90d2d8 100644 --- a/content/guides/observability.md +++ b/content/guides/observability.md @@ -2,8 +2,8 @@ title: "LiveTemplate Observability Guide" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/guides/OBSERVABILITY.md" -source_ref: "v0.22.0" -source_commit: "22a4853506a682583b511e470bdd1e6193f4d5fe" +source_ref: "v0.23.0" +source_commit: "8294ce439a46a6a1f92e2a77b8a4978c9e526cc6" --- # LiveTemplate Observability Guide @@ -409,6 +409,6 @@ func RequestIDMiddleware(next http.Handler) http.Handler { ## Related Documentation -- [ARCHITECTURE.md](https://github.com/livetemplate/livetemplate/blob/v0.22.0/docs/guides/ARCHITECTURE.md) - System architecture overview -- [internal/observe/](https://github.com/livetemplate/livetemplate/tree/v0.22.0/docs/internal/observe) - Package implementation +- [ARCHITECTURE.md](https://github.com/livetemplate/livetemplate/blob/v0.23.0/docs/design/ARCHITECTURE.md) - System architecture overview +- [internal/observe/](https://github.com/livetemplate/livetemplate/tree/v0.23.0/internal/observe) - Package implementation - [Go slog documentation](https://pkg.go.dev/log/slog) - Standard library reference diff --git a/content/guides/progressive-complexity.md b/content/guides/progressive-complexity.md index 5002f7d..3464f96 100644 --- a/content/guides/progressive-complexity.md +++ b/content/guides/progressive-complexity.md @@ -2,8 +2,8 @@ title: "Progressive Complexity Guide" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/guides/progressive-complexity.md" -source_ref: "v0.22.0" -source_commit: "22a4853506a682583b511e470bdd1e6193f4d5fe" +source_ref: "v0.23.0" +source_commit: "8294ce439a46a6a1f92e2a77b8a4978c9e526cc6" --- # Progressive Complexity Guide @@ -236,10 +236,11 @@ LiveTemplate offers three loading models. Pick the simplest one that fits your u |---|---|---|---| | Grey out the form | N/A | **7.1 — Auto** (`