diff --git a/cookbook/casbin/server.go b/cookbook/casbin/server.go index de19d25a..99bd771c 100644 --- a/cookbook/casbin/server.go +++ b/cookbook/casbin/server.go @@ -3,6 +3,7 @@ package main import ( "log/slog" "net/http" + "os" "github.com/casbin/casbin/v3" "github.com/golang-jwt/jwt/v5" @@ -10,6 +11,7 @@ import ( "github.com/labstack/echo/v5" ) +// docs:start middleware // NewCasbinMiddleware returns middleware for [Casbin](https://casbin.org/). func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { @@ -28,6 +30,8 @@ func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Contex } } +// docs:end middleware + /* Test with: curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" @@ -35,10 +39,13 @@ curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciO func main() { e := echo.New() + // docs:start enforcer ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") if err != nil { slog.Error("failed to initialize Casbin enforcer", "error", err) + os.Exit(1) } + // docs:end enforcer // BasicAuth middleware does authentication // - should pass `curl -v -u "alice:password" http://localhost:8080/dataset1/any` @@ -53,6 +60,7 @@ func main() { //} //e.Use(NewCasbinMiddleware(ce, basicAuthUser)) // Casbin does authorization + // docs:start jwt e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization token, err := echo.ContextGet[*jwt.Token](c, "user") @@ -62,6 +70,7 @@ func main() { return token.Claims.GetSubject() } e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization + // docs:end jwt e.GET("/*", func(c *echo.Context) error { return c.String(http.StatusOK, "Hello, World!") diff --git a/cookbook/hello-world/server.go b/cookbook/hello-world/server.go index 3903bf1c..d2a8d493 100644 --- a/cookbook/hello-world/server.go +++ b/cookbook/hello-world/server.go @@ -10,6 +10,7 @@ import ( func main() { // Echo instance + // docs:start hero e := echo.New() // Middleware @@ -20,6 +21,7 @@ func main() { e.GET("/", func(c *echo.Context) error { return c.String(http.StatusOK, "Hello, World!\n") }) + // docs:end hero // Start server sc := echo.StartConfig{Address: ":1323"} diff --git a/site/plugins/remark-source-code.mjs b/site/plugins/remark-source-code.mjs index 7c1e9b00..278644eb 100644 --- a/site/plugins/remark-source-code.mjs +++ b/site/plugins/remark-source-code.mjs @@ -66,7 +66,12 @@ function readSnippet(specifier, root) { } catch (error) { throw new Error(`Cannot include ${path}: ${error.message}`); } - const lines = source.split('\n'); + return sourceSnippet(source, region, path); +} + +/** Extract the same source regions from Vite raw imports and Markdown includes. */ +export function sourceSnippet(source, region, path = 'source') { + const lines = source.replace(/\r\n/g, '\n').split('\n'); if (region !== undefined) { const starts = []; const ends = []; diff --git a/site/plugins/remark-source-code.test.mjs b/site/plugins/remark-source-code.test.mjs index 423a25f1..19da7cda 100644 --- a/site/plugins/remark-source-code.test.mjs +++ b/site/plugins/remark-source-code.test.mjs @@ -5,7 +5,7 @@ import { join } from 'node:path'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; import { unified } from 'unified'; -import remarkSourceCode, { sourceReference, withSourceCode } from './remark-source-code.mjs'; +import remarkSourceCode, { sourceReference, sourceSnippet, withSourceCode } from './remark-source-code.mjs'; import { parseSourceDocument, visitCode } from './source-document.mjs'; import { localePrefixes } from '../src/locales.mjs'; @@ -212,3 +212,24 @@ test('every cookbook page in every locale displays source-owned programs and exc } } }); + +test('raw component imports and Markdown includes share source excerpts; Casbin middleware appears once per locale', () => { + const root = fileURLToPath(new URL('../..', import.meta.url)); + const path = 'cookbook/hello-world/server.go'; + const code = sourceSnippet(readFileSync(join(root, path), 'utf8'), 'hero', path); + const [included] = programFences(render(`\x60\x60\x60go file=${path}#hero\n\x60\x60\x60`, root)); + assert.equal(code, included.value); + assert.match(code, /e\.GET\("\/"/); + assert.doesNotMatch(code, /docs:(start|end)/); + for (const locale of localePrefixes) { + const page = join(root, 'site/src/content/docs', locale, 'middleware/casbin-auth.md'); + const rendered = render(readFileSync(page, 'utf8'), root, page); + const fences = programFences(rendered); + const declarations = fences.flatMap((fence) => [...fence.value.matchAll(/^func NewCasbinMiddleware\b/gm)]); + assert.equal(declarations.length, 1, `${locale}Casbin middleware has a single source-backed definition`); + assert.ok(fences.some((fence) => fence.value.includes('enforcer.Enforce('))); + assert.ok(fences.some((fence) => fence.value.includes('casbin.NewEnforcer(')), `${locale}the enforcer used by the excerpts is defined`); + assert.ok(fences.some((fence) => fence.value.includes('echojwt.JWT('))); + assert.ok(!fences.some((fence) => /^package main\b/m.test(fence.value)), 'the full server is linked rather than duplicated'); + } +}); diff --git a/site/src/components/HomeHero.astro b/site/src/components/HomeHero.astro index faff83ea..ddd6514b 100644 --- a/site/src/components/HomeHero.astro +++ b/site/src/components/HomeHero.astro @@ -1,10 +1,16 @@ --- // Living Terminal hero: code window + a terminal pane that types `curl` -// and streams Echo's JSON response. Animation is client-side, with a -// static final-state fallback for reduced-motion / no-JS. +// and shows the runnable example's response. Animation is client-side, with a +// static final-state fallback for reduced motion. import { starsLabel } from '../data/github.ts'; import stable from '../generated/echo-source.json'; +import { Code } from 'astro:components'; +import helloWorldSource from '../../../cookbook/hello-world/server.go?raw'; +import { sourceSnippet } from '../../plugins/remark-source-code.mjs'; const next = process.env.DOCS_CHANNEL === 'next'; +const helloWorldCode = sourceSnippet(helloWorldSource, 'hero', 'cookbook/hello-world/server.go'); +// The code window is aria-hidden, so Shiki's
 must not be a tab stop.
+const notFocusable = { pre(node: { properties: Record }) { delete node.properties.tabindex; } };
 ---
 
 
@@ -28,19 +34,9 @@ const next = process.env.DOCS_CHANNEL === 'next';