feat: publish the Sandwich Hime tutorial starter
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
<?sando go
|
||||
package views
|
||||
|
||||
func Badge(label string)
|
||||
?>
|
||||
<?# SPDX-License-Identifier: 0BSD ?>
|
||||
<span class="badge"><?= label ?></span>
|
||||
Generated
+37
@@ -0,0 +1,37 @@
|
||||
// Code generated by himesan; DO NOT EDIT.
|
||||
// himesan:compiler 0.1.0-dev
|
||||
// himesan:runtime-abi sando.v1
|
||||
// himesan:source-sha256 241093d3b845d20c38e5a0fe5cf2213d6bb9114138818dbe98d8b3a99ff19d24
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
__himesan_context "context"
|
||||
__himesan_sando "gamertan.com/sandwich-hime/sando"
|
||||
__himesan_io "io"
|
||||
)
|
||||
|
||||
var _ = __himesan_sando.ABI
|
||||
|
||||
func Badge(label string) __himesan_sando.Component {
|
||||
return __himesan_sando.ComponentFunc(func(__himesan_render_context __himesan_context.Context, __himesan_writer __himesan_io.Writer) error {
|
||||
_ = __himesan_render_context
|
||||
//line internal/views/badge.sando:5:3
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/badge.sando:6:37
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n<span class=\"badge\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/badge.sando:7:25
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (label)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/badge.sando:7:33
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</span>\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?sando go
|
||||
package views
|
||||
|
||||
func Home(view HomeView)
|
||||
?>
|
||||
<?# SPDX-License-Identifier: 0BSD ?>
|
||||
<main id="main" class="page">
|
||||
<p class="eyebrow"><?~ Badge("rendered on this request") ?></p>
|
||||
<h1>Hello, <?= view.Visitor ?>.</h1>
|
||||
<p class="lede">This page began as readable HTML, became typed Go, and received its data from an ordinary server handler just now.</p>
|
||||
|
||||
<section aria-labelledby="trail-heading">
|
||||
<h2 id="trail-heading">Choose a trail</h2>
|
||||
<div class="trail-grid">
|
||||
<? for _, trail := range view.Trails { ?>
|
||||
<article class="trail">
|
||||
<h3><a href="<?= trail.URL ?>"><?= trail.Label ?></a></h3>
|
||||
<p><?= trail.Description ?></p>
|
||||
</article>
|
||||
<? } ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="note" aria-label="Template boundary">
|
||||
<strong>Two moments, one calm path:</strong> Hime-san generated the component code when the template changed. The compiled application rendered this response with your request data now.
|
||||
</aside>
|
||||
</main>
|
||||
Generated
+81
@@ -0,0 +1,81 @@
|
||||
// Code generated by himesan; DO NOT EDIT.
|
||||
// himesan:compiler 0.1.0-dev
|
||||
// himesan:runtime-abi sando.v1
|
||||
// himesan:source-sha256 2f04e0f2e6caae73ea3d419a88c53afcaf25d300e76dc522ae2f418a03a68def
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
__himesan_context "context"
|
||||
__himesan_sando "gamertan.com/sandwich-hime/sando"
|
||||
__himesan_io "io"
|
||||
)
|
||||
|
||||
var _ = __himesan_sando.ABI
|
||||
|
||||
func Home(view HomeView) __himesan_sando.Component {
|
||||
return __himesan_sando.ComponentFunc(func(__himesan_render_context __himesan_context.Context, __himesan_writer __himesan_io.Writer) error {
|
||||
_ = __himesan_render_context
|
||||
//line internal/views/home.sando:5:3
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:6:37
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n<main id=\"main\" class=\"page\">\n <p class=\"eyebrow\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:8:26
|
||||
if __himesan_error := __himesan_sando.Render(__himesan_render_context, __himesan_writer, (Badge("rendered on this request"))); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:8:62
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</p>\n <h1>Hello, "); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:9:18
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (view.Visitor)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:9:33
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, ".</h1>\n <p class=\"lede\">This page began as readable HTML, became typed Go, and received its data from an ordinary server handler just now.</p>\n\n <section aria-labelledby=\"trail-heading\">\n <h2 id=\"trail-heading\">Choose a trail</h2>\n <div class=\"trail-grid\">\n "); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:15:10
|
||||
for _, trail := range view.Trails {
|
||||
//line internal/views/home.sando:15:48
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n <article class=\"trail\">\n <h3><a href=\""); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:17:28
|
||||
if __himesan_error := __himesan_sando.WriteURL(__himesan_writer, (trail.URL)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:17:40
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:17:46
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (trail.Label)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:17:60
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</a></h3>\n <p>"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:18:18
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (trail.Description)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:18:38
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</p>\n </article>\n "); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/home.sando:20:10
|
||||
}
|
||||
//line internal/views/home.sando:20:14
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n </div>\n </section>\n\n <aside class=\"note\" aria-label=\"Template boundary\">\n <strong>Two moments, one calm path:</strong> Hime-san generated the component code when the template changed. The compiled application rendered this response with your request data now.\n </aside>\n</main>\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?sando go
|
||||
package views
|
||||
|
||||
func Layout(view LayoutView)
|
||||
?>
|
||||
<?# SPDX-License-Identifier: 0BSD ?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="A tiny dynamic Go website built with Sandwich Hime.">
|
||||
<title><?= view.Title ?></title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; font-family: ui-rounded, system-ui, sans-serif; line-height: 1.6; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #f6f1e7; color: #28231d; }
|
||||
a { color: #74449a; text-underline-offset: .2em; }
|
||||
a:focus-visible { outline: .2rem solid #c85278; outline-offset: .2rem; }
|
||||
.skip { position: absolute; left: .75rem; top: -5rem; padding: .6rem .8rem; background: #fff; color: #28231d; z-index: 2; }
|
||||
.skip:focus { top: .75rem; }
|
||||
.page, footer { width: min(68rem, calc(100% - 2rem)); margin-inline: auto; }
|
||||
.page { padding-block: clamp(3rem, 9vw, 7rem); }
|
||||
.eyebrow { margin: 0 0 1rem; }
|
||||
.badge { display: inline-block; padding: .25rem .65rem; border: 1px solid currentColor; border-radius: 999px; font-size: .84rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
|
||||
h1 { max-width: 16ch; margin: 0; font-size: clamp(2.5rem, 9vw, 5.8rem); line-height: .98; overflow-wrap: anywhere; }
|
||||
h2 { margin-top: 3.5rem; font-size: clamp(1.6rem, 4vw, 2.4rem); }
|
||||
h3 { margin-top: 0; }
|
||||
.lede { max-width: 46rem; font-size: clamp(1.1rem, 2vw, 1.35rem); }
|
||||
.trail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 1rem; }
|
||||
.trail, .note { padding: 1.1rem; border: 1px solid #cdbfae; border-radius: .8rem; background: #fffaf1; }
|
||||
.trail p { margin-bottom: 0; }
|
||||
.note { max-width: 52rem; margin-top: 2rem; }
|
||||
footer { padding-block: 1.5rem 2.5rem; border-top: 1px solid #cdbfae; font-size: .9rem; }
|
||||
footer p { margin: .25rem 0; }
|
||||
code { overflow-wrap: anywhere; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background: #1d1a20; color: #f5eee5; }
|
||||
a { color: #d9a5ff; }
|
||||
.trail, .note { border-color: #5b505f; background: #29242d; }
|
||||
footer { border-color: #5b505f; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<?~ view.Body ?>
|
||||
<footer>
|
||||
<p>Rendered at <time datetime="<?= view.RenderedAtUTC ?>"><?= view.RenderedAtUTC ?></time> as request <strong>#<?= view.RequestNumber ?></strong> since this process started.</p>
|
||||
<p>This HTML response is not cached. Refresh it and the server builds fresh typed data.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+69
@@ -0,0 +1,69 @@
|
||||
// Code generated by himesan; DO NOT EDIT.
|
||||
// himesan:compiler 0.1.0-dev
|
||||
// himesan:runtime-abi sando.v1
|
||||
// himesan:source-sha256 e56b373194349ae32dcbad4a26c1d1a3d133e335ff916cef064e12ca2901adf3
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
__himesan_context "context"
|
||||
__himesan_sando "gamertan.com/sandwich-hime/sando"
|
||||
__himesan_io "io"
|
||||
)
|
||||
|
||||
var _ = __himesan_sando.ABI
|
||||
|
||||
func Layout(view LayoutView) __himesan_sando.Component {
|
||||
return __himesan_sando.ComponentFunc(func(__himesan_render_context __himesan_context.Context, __himesan_writer __himesan_io.Writer) error {
|
||||
_ = __himesan_render_context
|
||||
//line internal/views/layout.sando:5:3
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:6:37
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <meta name=\"description\" content=\"A tiny dynamic Go website built with Sandwich Hime.\">\n <title>"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:13:14
|
||||
if __himesan_error := __himesan_sando.WriteRCDATA(__himesan_writer, (view.Title)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:13:27
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</title>\n <style>\n :root { color-scheme: light dark; font-family: ui-rounded, system-ui, sans-serif; line-height: 1.6; }\n * { box-sizing: border-box; }\n body { margin: 0; background: #f6f1e7; color: #28231d; }\n a { color: #74449a; text-underline-offset: .2em; }\n a:focus-visible { outline: .2rem solid #c85278; outline-offset: .2rem; }\n .skip { position: absolute; left: .75rem; top: -5rem; padding: .6rem .8rem; background: #fff; color: #28231d; z-index: 2; }\n .skip:focus { top: .75rem; }\n .page, footer { width: min(68rem, calc(100% - 2rem)); margin-inline: auto; }\n .page { padding-block: clamp(3rem, 9vw, 7rem); }\n .eyebrow { margin: 0 0 1rem; }\n .badge { display: inline-block; padding: .25rem .65rem; border: 1px solid currentColor; border-radius: 999px; font-size: .84rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }\n h1 { max-width: 16ch; margin: 0; font-size: clamp(2.5rem, 9vw, 5.8rem); line-height: .98; overflow-wrap: anywhere; }\n h2 { margin-top: 3.5rem; font-size: clamp(1.6rem, 4vw, 2.4rem); }\n h3 { margin-top: 0; }\n .lede { max-width: 46rem; font-size: clamp(1.1rem, 2vw, 1.35rem); }\n .trail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 1rem; }\n .trail, .note { padding: 1.1rem; border: 1px solid #cdbfae; border-radius: .8rem; background: #fffaf1; }\n .trail p { margin-bottom: 0; }\n .note { max-width: 52rem; margin-top: 2rem; }\n footer { padding-block: 1.5rem 2.5rem; border-top: 1px solid #cdbfae; font-size: .9rem; }\n footer p { margin: .25rem 0; }\n code { overflow-wrap: anywhere; }\n @media (prefers-color-scheme: dark) {\n body { background: #1d1a20; color: #f5eee5; }\n a { color: #d9a5ff; }\n .trail, .note { border-color: #5b505f; background: #29242d; }\n footer { border-color: #5b505f; }\n }\n </style>\n</head>\n<body>\n <a class=\"skip\" href=\"#main\">Skip to content</a>\n "); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:47:7
|
||||
if __himesan_error := __himesan_sando.Render(__himesan_render_context, __himesan_writer, (view.Body)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:47:19
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n <footer>\n <p>Rendered at <time datetime=\""); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:49:40
|
||||
if __himesan_error := __himesan_sando.WriteAttr(__himesan_writer, (view.RenderedAtUTC)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:49:61
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:49:67
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (view.RenderedAtUTC)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:49:88
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</time> as request <strong>#"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:49:120
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (view.RequestNumber)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line internal/views/layout.sando:49:141
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</strong> since this process started.</p>\n <p>This HTML response is not cached. Refresh it and the server builds fresh typed data.</p>\n </footer>\n</body>\n</html>\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
package views
|
||||
|
||||
import "gamertan.com/sandwich-hime/sando"
|
||||
|
||||
// Trail is one server-provided destination rendered by Home.
|
||||
type Trail struct {
|
||||
Label string
|
||||
Description string
|
||||
URL string
|
||||
}
|
||||
|
||||
// HomeView is the complete typed input to the inner page component.
|
||||
type HomeView struct {
|
||||
Visitor string
|
||||
Trails []Trail
|
||||
}
|
||||
|
||||
// LayoutView is the typed input to the full-document component.
|
||||
type LayoutView struct {
|
||||
Title string
|
||||
Body sando.Component
|
||||
RenderedAtUTC string
|
||||
RequestNumber uint64
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gamertan.com/sandwich-hime/sando"
|
||||
)
|
||||
|
||||
func TestHomeEscapesUntrustedTextAndNestsBadge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const attack = `<script>alert("no")</script>`
|
||||
var output strings.Builder
|
||||
page := Home(HomeView{Visitor: attack})
|
||||
if err := sando.Render(context.Background(), &output, page); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rendered := output.String()
|
||||
if strings.Contains(rendered, attack) || strings.Contains(rendered, "<script>") {
|
||||
t.Fatalf("visitor became markup: %s", rendered)
|
||||
}
|
||||
if !strings.Contains(rendered, "<script>") {
|
||||
t.Fatalf("escaped visitor is missing: %s", rendered)
|
||||
}
|
||||
if !strings.Contains(rendered, `<span class="badge">rendered on this request</span>`) {
|
||||
t.Fatalf("nested Badge component is missing: %s", rendered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomeRejectsDangerousURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var output strings.Builder
|
||||
page := Home(HomeView{Trails: []Trail{
|
||||
{Label: "unsafe", URL: "javascript:alert(1)"},
|
||||
}})
|
||||
if err := sando.Render(context.Background(), &output, page); err == nil {
|
||||
t.Fatal("dangerous URL rendered without an error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user