feat: source-include for literate docs (A2/A3)#256
Conversation
Adds an `include="..."` fence attribute that pulls a file slice into the rendered code block, paired with `embed-lvt` for the running widget. Together they deliver the literate "read the code, see it run" experience without any new runtime — the snippets are sliced from the same `.go`/`.tmpl` files the deployed app uses, so there's no drift between docs and production. Authoring shape: ````markdown The state — pure data, cloned per session: ```go include="./_app/counter.go" lines="5-8" ``` The handler — note line 20: ```go include="./_app/counter.go" lines="13-35" highlight="20" ``` The template: ```html include="./_app/counter.tmpl" lines="10-13" ``` Click below — your changes round-trip to the deployed app: ```embed-lvt path="/apps/counter/" upstream="http://127.0.0.1:9090" ``` ```` Five include features in this PR: - **lines="N-M"** — single line range; auto-clamps if file shrinks - **lines="N-M,P-Q"** — multiple ranges joined with a language- appropriate ellipsis comment (`// ...` for Go, `# ...` for YAML, `<!-- ... -->` for HTML, etc.) - **region="name"** — extract content between `// >>> region:name` / `// <<< region:name` markers; survives line-number drift, works with any single-line comment style - **highlight="N-M"** — Prism Line Highlight overlay; numbers are file-absolute (match the gutter) - **Source-link footer** — auto-generated `counter.go:13-35` link to the cited file at the cited range when frontmatter declares `source_repo` + `source_path`. Default ref tracks the running tinkerdown binary's release version (set via ldflags), so docs always link at the matching tag A2 example: `examples/literate-counter-include/` — a real deployable counter app (`_app/main.go`) with state struct and `Increment`/`Decrement`/`Reset` handlers, paired with a docs page that includes ranges from each. A3 example: `examples/literate-linked-include/` — two `embed-lvt` blocks against the same upstream with `session="tour"`. The counter app uses `ctx.BroadcastAction("Increment", nil)` and a constant-groupID authenticator so all connections share state — clicking in one region updates both, and either matches a tab visiting :9090 directly. Implementation: - `internal/include/include.go` — Resolve, Slice, SliceRanges, Dedent, FindRegion, LanguageEllipsis, Preprocess(WithLinks). Emits raw HTML so `<pre>` carries `class="line-numbers"`, optional `data-line=`, `data-start=`, `data-line-offset=` for Prism plugins to read. - `parser.go` + `page.go` — preprocess includes after auto-tasks / auto-tables; track `IncludedFiles` per page. - `internal/server/watcher.go` — `SetExtraFiles` so the file watcher fires reload on included-file changes too. - `internal/server/server.go` — wires Prism Line Highlight + Line Numbers plugins (vendored from prismjs@1.29.0). - `cmd/tinkerdown/main.go` — propagates the binary's build version to `tinkerdown.DefaultSourceRef` so footer links target the running release. Bug fixes found during testing: - `client/src/blocks/embed-lvt-block.ts`: rename pending wrapper IDs to be unique-per-block. Without this, two embeds pointing at the same upstream collide on LiveTemplate's event-delegator key (livetemplate generates one wrapperID per template, not per session) — clicks in one region silently route to the other's delegator. Surfaced while building the linked example. - Nested fence handling in `Preprocess` — markdown examples that contain `LANG include="..."` text inside a wrapping fence now pass through verbatim instead of accidentally triggering substitution. Tests: - 28 unit tests in `internal/include/include_test.go` covering line-range parsing, path confinement, dedent, slice, multi- range, named regions, highlight, link-footer rendering, nested- fence pass-through, and the full Preprocess loop. - 3 chromedp e2e tests in `include_e2e_test.go` covering snippet rendering, hot reload on file change, and path confinement. - Manual iPhone-over-Tailscale validation: handlers, state struct, template all rendered with line numbers; broadcast call highlighted via Prism overlay; cross-region and cross-tab sync verified. Out of scope for this PR (deferred): - Multi-range gutter numbering with non-contiguous file lines (e.g. lines="5-8,15-22" currently shows gutter 5,6,7,8,9,10... rather than skipping) - Remote includes (`include="https://..."`) - WASM/child-process Go execution for inline `go server` blocks (the include= primitive plus `embed-lvt` covers the literate use case without a new runtime) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #256:
|
There was a problem hiding this comment.
Pull request overview
This PR adds a literate-documentation “source include” feature that lets markdown code fences pull and render slices of real source files (with optional line highlights and source-link footers), and wires the server/file-watcher so included-file edits trigger hot reload—complementing embed-lvt for “read the code, see it run” docs.
Changes:
- Add
include="..."(withlines=,region=,highlight=) preprocessing that substitutes fenced blocks with sliced file contents and optional GitHub source links. - Track per-page
IncludedFilesand update the watcher/server reload path to re-discover + reload when an included file changes. - Vendor Prism line-numbers/line-highlight plugins and fix
embed-lvtwrapper ID collisions for multiple embeds of the same upstream.
Reviewed changes
Copilot reviewed 24 out of 31 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tinkerdown.go | Adds DefaultSourceRef and Page.IncludedFiles to support include-footers + watcher tracking. |
| parser.go | Adds source_ref frontmatter field for source-link pinning. |
| page.go | Runs include preprocessing and stores IncludedFiles on the page. |
| internal/server/watcher.go | Adds an “extra files” set so non-.md included files can trigger reloads. |
| internal/server/server.go | Pushes included-file paths into the watcher; reloads on included-file changes; serves Prism plugins; adds footer CSS. |
| internal/include/include.go | New include resolver/slicer/preprocessor implementation (path confinement, ranges/regions, dedent, footer). |
| internal/include/include_test.go | Unit tests for include parsing/slicing/regions/footers. |
| internal/assets/assets.go | Exposes getters for Prism line-highlight/line-numbers plugin assets. |
| internal/assets/vendor/prism/prism-line-numbers.min.js | Vendored Prism line-numbers plugin JS. |
| internal/assets/vendor/prism/prism-line-numbers.min.css | Vendored Prism line-numbers plugin CSS. |
| internal/assets/vendor/prism/prism-line-highlight.min.js | Vendored Prism line-highlight plugin JS. |
| internal/assets/vendor/prism/prism-line-highlight.min.css | Vendored Prism line-highlight plugin CSS. |
| internal/assets/client/tinkerdown-client.browser.js | Updates embed-lvt wrapper ID behavior in the built client bundle. |
| client/src/blocks/embed-lvt-block.ts | Makes data-lvt-id unique per embed block to avoid event-delegator collisions. |
| cmd/tinkerdown/main.go | Propagates release version into tinkerdown.DefaultSourceRef for source-link pinning. |
| include_e2e_test.go | Adds e2e coverage for include rendering, hot reload, and path confinement. |
| docs/guides/literate-docs.md | Documents include= authoring, ranges/regions/highlight, hot reload, and source-link footers. |
| examples/literate-counter-include/index.md | Adds an example page using include fences + embed-lvt. |
| examples/literate-counter-include/README.md | Adds run instructions for the example. |
| examples/literate-counter-include/_app/* | Adds a demo LiveTemplate counter app used as “real source” for includes. |
| examples/literate-linked-include/index.md | Adds a linked-embeds example demonstrating shared state across multiple embeds. |
| examples/literate-linked-include/_app/* | Adds the linked-embeds counter app. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Emit a fresh fence with the same fence/lang and the body | ||
| // replaced. We deliberately drop include="..." and lines="..." | ||
| // from the rendered fence info so the post-render code block | ||
| // looks identical to a hand-authored one — clean Prism class, | ||
| // clean DOM, no leaked authoring metadata. | ||
| cleanInfo := rest | ||
| cleanInfo = strings.ReplaceAll(cleanInfo, incMatch[0], "") | ||
| if l := linesAttrRe.FindString(rest); l != "" { | ||
| cleanInfo = strings.ReplaceAll(cleanInfo, l, "") | ||
| } | ||
| if r := regionAttrRe.FindString(rest); r != "" { | ||
| cleanInfo = strings.ReplaceAll(cleanInfo, r, "") | ||
| } | ||
| if h := highlightAttrRe.FindString(rest); h != "" { | ||
| cleanInfo = strings.ReplaceAll(cleanInfo, h, "") | ||
| } | ||
| cleanInfo = strings.TrimSpace(cleanInfo) | ||
| // Emit raw HTML for every include — gives us the <pre> handle | ||
| // that Prism's Line Numbers plugin (always on) and Line | ||
| // Highlight plugin (when highlight= is set) read their config | ||
| // from. Goldmark passes raw HTML through under allowRawHTML | ||
| // for trusted file-based content. |
| // backticks, capturing fence chars, language, and the rest of the | ||
| // info string. Only standard backtick fences are recognized in v1 | ||
| // (tilde fences exist in CommonMark but tinkerdown's own examples | ||
| // universally use backticks). | ||
| var fenceOpenerRe = regexp.MustCompile("^(`{3,})([A-Za-z0-9_+-]*)(.*)$") |
| // contains `include="..."`, replaces the (empty) fence body with the | ||
| // resolved file slice, and returns the transformed content plus the | ||
| // absolute paths of every included file (for the watcher to track) | ||
| // plus any warnings (bad path, missing file, range error). Bad | ||
| // includes pass through with the original empty body so the page | ||
| // still renders — same posture as the embed-lvt unavailable badge. |
| pages := s.collectPagesForAutoRoutes() | ||
| for _, page := range pages { | ||
| for _, f := range page.IncludedFiles { | ||
| if f == abs { | ||
| return true |
| replace ( | ||
| github.com/livetemplate/livetemplate => ../../../../../../livetemplate | ||
| github.com/livetemplate/lvt => ../../../../../../lvt | ||
| ) |
| replace ( | ||
| github.com/livetemplate/livetemplate => ../../../../../../livetemplate | ||
| github.com/livetemplate/lvt => ../../../../../../lvt | ||
| ) |
| // repository + the page's own relative path). | ||
| SourceRepo string `yaml:"source_repo,omitempty"` // e.g. "https://github.com/livetemplate/livetemplate" | ||
| SourcePath string `yaml:"source_path,omitempty"` // e.g. "docs/guides/progressive-complexity.md" | ||
| SourceRef string `yaml:"source_ref,omitempty"` // git ref for source links (tag/branch/commit). Defaults to "main". |
Resolves the Claude- and Copilot-bot review on the source-include
PR. Highlights:
- Examples buildable from a fresh clone. Both example go.mod files
drop the `replace ../../../../../../{livetemplate,lvt}` directives
that assumed a sibling-repo layout, and pin published
`livetemplate v0.8.23` + `lvt v0.1.6` from the public proxy. The
go directive is set to released `1.22` (was unreleased `1.26.0`).
Trimmed the test-deps that bled in via the old replace path.
- Prism Line Numbers + Line Highlight plugins are gated on
`len(page.IncludedFiles) > 0`. The ~30 KB of CSS/JS used to load
on every rendered page; non-include pages now pay nothing.
- isIncludedFile race fix. Acquire `s.mu.RLock` around the page-
route iteration and short-circuit on `.md` events so the watcher
callback isn't redundantly scanning during Discover.
- CommonMark-compliant fence openers in the include preprocessor
regex (allow up to 3 leading spaces) so include= fences inside
lists/quotes are recognized.
- Empty-body validation: include fences with non-empty body now
emit a warning instead of silently discarding the content.
- Stdlib HTML/URL escaping. Hand-rolled `escapeAttr`/`escapeText`
replaced with thin wrappers around `html.EscapeString`; URL path
segments are `url.PathEscape`d before joining.
- `source_path` accepts file or directory. Auto-detects via
`filepath.Ext`: empty → directory; otherwise call Dir as before.
- `source_ref` resolution order documented in the Frontmatter
struct (field → DefaultSourceRef → "main").
- embed-lvt e2e test caught up to client behavior. The
data-lvt-id collision fix in 8f48d84 made the client suffix the
upstream id with the block id, but the e2e test still asserted
exact equality. Now asserts the prefix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review -- PR 256: source-include for literate docs (A2/A3)OverviewThis PR delivers a clean, well-scoped feature: the include=... fence attribute lets docs pages cite line ranges from real source files. The implementation -- preprocess markdown before goldmark, emit raw HTML pre blocks so Prism plugins can read their config attributes -- is pragmatic and fits the existing codebase. The bug fix for colliding data-lvt-id values on same-upstream embeds is well-motivated and correctly handled. SecurityrenderSourceFooter -- source_repo is not validated as an http(s) URL. link.RepoURL comes directly from the source_repo frontmatter field. If an author sets source_repo to a javascript: URI, the generated footer link is a working XSS vector. The escapeAttr call prevents quotes from breaking the attribute, but a javascript: scheme passes through untouched. Add a scheme allowlist check (reject anything not starting with https:// or http://) before constructing the href. Since frontmatter is author-controlled the blast radius is small, but the fix is a one-liner. Path-traversal confinement -- solid. The EvalSymlinks-before-filepath.Rel approach correctly handles macOS /tmp -> /private/tmp aliasing. The e2e confinement test using an absolute path for the secret file is a good edge case to cover. Correctnessembed-lvt-block.ts: removed null-guard changes behavior on missing attribute. The old code guarded setAttribute with a null-check; the new code using the empty-string fallback always writes data-lvt-id with a leading dash when the original attribute is absent. In practice the attribute should always be present, but an early-return guard would make the intent explicit. FindRegion -- duplicate region names use first occurrence silently. If the start marker appears twice in a file (copy-paste mistake), the first pair is used with no diagnostic. A warn-on-duplicate would help authors catch stale markers. Test CoverageTestInclude_HotReload does not test the WebSocket push path. The test manually calls srv.Discover() then navigates again, verifying re-rendering picks up new content -- but not that the watcher SetExtraFiles / event-loop path actually fires a WebSocket reload. The watcher integration is the new piece; a follow-up test that mutates the file, waits for the WebSocket broadcast, and asserts the DOM updated without a full navigation would be more faithful. Missing unit tests worth adding:
Minor IssuesRepetitive Prism-asset handlers in server.go. The four new handlers (prism-line-highlight.js/css, prism-line-numbers.js/css) are structurally identical. A small helper would reduce the duplication. Style only. Both example _app directories share the same go.mod module path. literate-counter-include/_app and literate-linked-include/_app both declare module example.com/literate-counter. This causes go work confusion if someone adds them to a workspace. The linked example should use example.com/literate-linked-counter. highlight= plus multi-range lines= misalignment is not surfaced to authors. The PR notes that only the first range start feeds data-start, so highlight= in a multi-range block is off for ranges after the first. Consider emitting a warning log when highlight= is combined with comma-separated lines=. DefaultSourceRef is a mutable package-level variable. Acceptable for set-once-at-startup, but parallel tests that mutate it would race. A server config field or functional option would scale better. Strengths
SummaryThe core approach is sound and the implementation is clean. Three things worth resolving before merge:
The hot-reload test gap and multi-range highlight limitation are good follow-up tickets rather than blockers. Generated with Claude Code (https://claude.com/claude-code) |
Three blockers from the post-push Claude bot review:
- Reject non-http(s) `source_repo` schemes in renderSourceFooter.
Frontmatter is author-controlled but a `javascript:` or `data:`
URL would inject XSS through the rendered <a href="...">.
Allowlist beats escape-only here. Covered by a new unit test.
- Restore explicit null-guard in embed-lvt-block.ts. The previous
`getAttribute(...) || ""` fallback would silently write
`data-lvt-id="-{blockId}"` if the attribute were ever absent;
now an `if (original)` early-skip makes the intent explicit.
- Rename `examples/literate-linked-include/_app/go.mod` from
`module example.com/literate-counter` to
`module example.com/literate-linked-counter` so the two example
modules don't collide if a contributor adds them to a go.work.
Also re-built the embedded client browser bundle to match the TS
source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review: PR #256 —
|
| Area | Unit | E2E |
|---|---|---|
| Line range parsing | ✅ | ✅ |
| Path confinement | ✅ | ✅ |
| Dedent | ✅ | — |
| Slice / multi-range | ✅ | — |
| Named regions | ✅ | — (no E2E) |
| Hot reload | — | ✅ |
| Source link rendering | ❌ | ❌ |
| Multi-range + highlight interaction | ❌ | ❌ |
CLAUDE.md Note
The CLAUDE.md instructs checking livetemplate/client before implementing lvt-* attributes. This PR does not introduce a lvt-* attribute — the include= feature is a Tinkerdown-level Markdown preprocessor, not a LiveTemplate fence. No conflict.
Overall: Ready to merge with the file-size guard and source-link unit tests addressed. The multi-range + highlight warning is a nice-to-have but not a blocker given the limitation is already documented.
🤖 Generated with Claude Code
The post-round-2 review surfaced two pre-merge gates plus a small cleanup. All addressed: - File-size cap on include reads. Added `maxIncludeBytes = 10 MB` and a `readFileBounded` helper that stat-checks before `os.ReadFile`. Avoids accidentally pulling a vendored binary or giant log into the docs build (silent OOM was the previous failure mode). New unit test verifies oversized files are rejected with a clear "too large" error. - Source-link rendering tests filled in. Added a table-driven rejection test for non-http(s) schemes (now includes ftp:// per the bot's request), plus a dedicated test for the "include lives outside the page directory" branch where the footer is silently omitted. - Dropped the redundant `rel == ".."` clause in path-confinement — it's already covered by `strings.HasPrefix(rel, "..")`. Pure code-clarity win. Two non-blocker items intentionally skipped per the bot's own classification: - Multi-range + highlight overlay misalignment warning: the limitation is documented at include.go:220-226 and in the user guide; surfacing it as a build warning is nice-to-have, not a merge gate. - Region duplicate-marker warning: real-world frequency is low, and the current behavior (use first pair) produces a coherent snippet rather than a crash. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #256:
|
The round-3 bot review flagged a "worth verifying" question about locking around `collectPagesForAutoRoutes` and a request to make the `DefaultSourceRef` global's contract explicit. Verified, no behavior change — just doc comments: - `collectPagesForAutoRoutes` is itself lock-free, so callers must hold s.mu (read or write). Both existing callers honor this: refreshWatcherIncludes runs under the write lock from Discover; isIncludedFile takes s.mu.RLock before calling. Documented the contract on the function so future callers know. - `DefaultSourceRef` is a single-writer, set-once-at-startup global. Production sets it during main() before goroutines launch. Tests that need a different ref should use the per-page `source_ref` frontmatter field rather than mutating the global. Documented the concurrency contract on the var declaration. No code change was needed: the locking concern was unfounded once verified, and the global is correctly used. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review —
|
The round-4 bot review left three asks. The first (DefaultSourceRef concurrency contract) was already addressed in round 4. The remaining two are pure documentation: - Comment at the `include.PreprocessWithLinks` call site in page.go explaining why `baseDir == root` is intentional (each page's includes are confined to its own directory tree). Records the decision so future readers don't think it's a bug. - README note in both examples pointing out that the sibling `_app/` directories are intentionally near-duplicates so each example can be cloned and run independently, and that bumps to `livetemplate` / `lvt` need to land in both go.mod files together. Also added a top-level README to literate-linked-include/ that didn't have one, pointing back at the counter example for setup instructions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #256:
|
| Area | Assessment |
|---|---|
| Security (path confinement, XSS, size limit) | ✅ Strong |
| Code quality / Go idioms | ✅ Strong |
| Test coverage | ✅ Good (minor gaps noted) |
| TypeScript bug fix | ✅ Correct and minimal |
| Documentation | ✅ Thorough |
| Concurrency handling | ✅ Correct with documented contracts |
| Performance | ✅ Acceptable (O(n²) fence scan noted) |
This is ready to merge once #251 lands and the base rebases to main. The implementation is solid, the security posture is careful, and the test coverage is comprehensive.
🤖 Generated with Claude Code
Round-5 bot review asked for a quick comment confirming that
LiveTemplateClient.connect() matches the wrapper id by exact value,
not by prefix — so the unique-per-block suffix (`${original}-${this.id}`)
that fixes the event-delegator collision doesn't break the connect
path. Documented inline at the rename site so a future reader doesn't
wonder.
Comment-only change; the minified bundle is byte-identical so no
asset rebuild was needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — feat: source-include for literate docs (A2/A3)Good feature overall. The path-confinement design is solid, the graceful-degradation posture (bad includes pass through instead of failing the page) is the right call, and the nested-fence fix for example authoring is a subtle but important correctness detail. A few things worth addressing before merging: Potential deadlock in
|
The round-6 review surfaced three actionable items (the rest were re-raises of already-addressed concerns or restated nits): - Replace `chromedp.Sleep(2*time.Second)` with `chromedp.WaitVisible` in TestInclude_HotReload and the include test helper. The fixed sleeps were a flakiness source under load. Picking selectors that must exist on a successful render (`pre.language-go` for the rendered include block; `body` for generic page navigation) makes the wait both shorter and more deterministic. The TestInclude e2e suite goes from ~28 s to ~13 s on this box. - Document escapeAttr/escapeText safety contract: both are `html.EscapeString` wrappers, and that's correct here only because the href is built from a scheme-allowlisted RepoURL plus per-segment `url.PathEscape`, not raw author input. Future code changes that introduce untrusted input into href construction must add their own sanitisation pass. - Document the fence-closer approximation in PreprocessWithLinks: CommonMark allows ≥ opener-count backticks for closers; we match exact length via TrimSpace equality. Adequate for docs we generate (authors close with what they opened), and a known trade-off worth recording at the closer-search loop. Items intentionally not re-addressed (already covered in earlier rounds): the locking concern (collectPagesForAutoRoutes is documented as lock-free with caller-holds-lock contract); the duplicate _app/ modules (README notes in both directories already explain why and what to keep in sync). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #256:
|
Round-7 review's only "should address" item: isIncludedFile had no direct automated coverage. Hot-reload was tested end-to-end but the watcher callback's `is this file one I care about?` predicate ran without a unit test that pins its behavior. Three cases exercised: - A `.go` file referenced by a page's `include="..."` returns true after Discover(). - An unrelated `.go` file returns false. - A `.md` path short-circuits to false regardless (those go down the page-file path; the early-return prevents redundant scans of the include set on every markdown save). Catches the kinds of regressions the bot was worried about: a future change to refreshWatcherIncludes / SetExtraFiles that drops the include set, or a refactor of isIncludedFile that loses the .md short-circuit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — feat: source-include for literate docs (A2/A3)Overall this is a well-scoped and thoughtfully implemented feature. The package architecture is clean, security posture is solid, and the test coverage is good for a first version. A few items worth addressing before merge. OverviewAdds Potential Bugs
Multi-range if start, err := strconv.Atoi(dataStart); err == nil && start > 1 {
sb.WriteString(fmt.Sprintf(` data-line-offset="%d"`, start-1))
}For
SecurityGood: Good: Good: Minor — Prism assets served without cache-busting keys. Code Quality
</html>`, wsURL, showSidebar, page.Title, seoTags, stylingOverrideCSS, prismIncludeCSS, sidebar, contentWithNav, defaultTheme, prismIncludeJS, mermaidScript, chartScript)This is pre-existing technical debt, but adding two more args makes it harder to maintain. Worth a follow-up to switch to a struct-based template (even
func escapeAttr(s string) string { return html.EscapeString(s) }
func escapeText(s string) string { return html.EscapeString(s) }The comment explains the intent (document call-site context), which is reasonable. Minor nit: in an HTML attribute context,
Test CoverageGood coverage overall: 28 unit tests across Missing unit test: no case for the Missing unit test: CLAUDE.md CheckThe project instructions say to verify that Summary
The hot-reload test gap and the multi-range highlight comment are the two items I'd want addressed before merge. The rest are nits or v2 considerations. 🤖 Generated with Claude Code |
Bootstraps CHANGELOG.md and documents the literate-authoring toolkit shipped via PR #251 (which absorbed PR #256): - show-source / hide-source flags on lvt blocks (+ lvt_show_source frontmatter) - embed-lvt block + every attribute (path/upstream/server/session/ timeout/height/show-source) - include="..." with lines/region/highlight + auto source-link footer (source_repo + source_path frontmatter, ldflags-driven default ref) - vendored Prism Line Numbers + Line Highlight plugins (gated on len(IncludedFiles) > 0) - file watcher reloads on included-file changes via SetExtraFiles After merge: ./release.sh 0.2.0 cuts the v0.2.0 tag. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds an
include="..."fence attribute that pulls a file slice into the rendered code block. Paired withembed-lvtfrom #251, this delivers the literate "read the code, see it run" experience without any new runtime — snippets are sliced from the same.go/.tmplfiles the deployed app uses, so there's no drift between docs and production.Authoring
Five include features
lines="5-8"lines="5-8,15-22"// ...,# ...,<!-- ... -->)region="state"+// >>> region:state/// <<< region:statemarkershighlight="20"(file-absolute)source_repo+source_pathfrontmattercounter.go:13-35link to the cited file at the cited rangeThe default git ref for footer links is set from
tinkerdown.DefaultSourceRef, whichcmd/tinkerdown/main.gopopulates with the binary's release version (via existing ldflags). Released docs always link at the matching tag;devbuilds fall back tomain.Examples
examples/literate-counter-include/— A2: real deployable counter app (_app/) with state struct +Increment/Decrement/Resethandlers, paired with a docs page that includes ranges from each.examples/literate-linked-include/— A3: twoembed-lvtblocks against the same upstream withsession="tour". The counter usesctx.BroadcastAction("Increment", nil)and a constant-groupID authenticator so all connections share state. Clicking in one region updates both, and either matches a tab visiting:9090directly.Implementation
internal/include/include.go—Resolve,Slice,SliceRanges,Dedent,FindRegion,LanguageEllipsis,Preprocess/PreprocessWithLinksparser.go+page.go— preprocess includes after auto-tasks/auto-tables; trackIncludedFilesper pageinternal/server/watcher.go—SetExtraFilesso the file watcher fires reload on included-file changesinternal/server/server.go— wires Prism Line Highlight + Line Numbers plugins (vendored from prismjs@1.29.0)include=block carriesclass="line-numbers",data-start=, optionaldata-line=/data-line-offset=Bug fixes found during testing
client/src/blocks/embed-lvt-block.ts: rename pending wrapper IDs to be unique-per-block. Without this, two embeds pointing at the same upstream collide on LiveTemplate's per-id event-delegator key (livetemplate generates one wrapperID per template, not per session) — clicks in one region silently route to the other's delegator. Surfaced building the linked example. Tightly relevant to feat: literateshow-sourceandembed-lvtfor rich interactive docs #251 but landing here since that PR isn't merged yet.Preprocess— markdown examples that containLANG include="..."text inside a wrapping fence (e.g. inside theliterate-docs.mdguide itself) now pass through verbatim instead of accidentally triggering substitution.Test plan
GOWORK=off go test -tags=ci -count=1 ./...— 16 packages greeninternal/includeunit tests: 28 cases (line-range parsing, path confinement, dedent, slice, multi-range, named regions, highlight, link-footer, nested-fence pass-through, full Preprocess loop)TestInclude_RendersSlice,TestInclude_HotReload,TestInclude_PathConfinement:9090direct ↔ embed) sync verifiedOut of scope (deliberately deferred)
lines="5-8,15-22"displays gutter 5,6,7,8,9,10... rather than skipping)include="https://...")go serverblocks — the include= primitive plusembed-lvtcovers the literate use case without a new runtime🤖 Generated with Claude Code