Deleting the widows feature answered one question; the human asked the general one. So every feature built so far is now audited, with the verdicts as a table in ADR-0046, and each remaining queue entry carries a layer note before anyone writes code for it. The codebase turned out to be otherwise clean, and I checked rather than remembered: no Go file writes a tag, a class or a style. ADR-0036's rule that all markup comes from a fragment had already forced that. One real finding. A tag listing received only `.Groups`, so the engine had decided that a tag listing *looks* grouped — ADR-0032's own reasoning was "so a busy term stays readable", which is a readability judgement. It now receives both shapes: the partition, because a template cannot group for itself, and the flat list, because choosing between them is markup. `Item` gained `.Section` so a flat listing can still say where an entry came from. Two judgement calls recorded rather than left implicit. The typographer stays: turning `--` into an en dash is a character transformation no stylesheet can express. Chrome strings stay: translations are data, and the alternative is every theme hardcoding Bengali month names. The inlined stylesheet is accepted with its cost written down — bytes per page, no caching — and a trigger for revisiting it. Two patterns worth reusing came out of this: offer the shape rather than choosing it, and a split feature is normal — search will be an engine-built index queried by the browser, not one or the other.
199 lines
6.0 KiB
Go
199 lines
6.0 KiB
Go
package render
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"khosra/internal/content"
|
|
)
|
|
|
|
func TestBundleRendersMarkdownIntoTheTheme(t *testing.T) {
|
|
r, err := New(nil, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := content.Parse("posts/hello.md", []byte("---\ntitle: Hello\n---\n\n# Heading\n\nSome *prose*.\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := r.Bundle(b, "en", []string{"en"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(out)
|
|
for _, want := range []string{
|
|
"<!doctype html>", `<html lang="en">`, "<title>Hello</title>",
|
|
"<h1>Hello</h1>", "<em>prose</em>", "<style>",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("output missing %q\n---\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBundleWithoutTitleFallsBackToKey(t *testing.T) {
|
|
r, err := New(nil, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := content.Parse("status/note.md", []byte("just a note\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := r.Bundle(b, "en", []string{"en"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(out), "<title>status/note</title>") {
|
|
t.Errorf("a titleless bundle must still produce a title element:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestSiteOverridesOneBlockAndInheritsTheRest(t *testing.T) {
|
|
siteFS := fstest.MapFS{
|
|
"templates/page.html": {Data: []byte(`{{define "main"}}<section class="mine">{{.Title}}</section>{{end}}`)},
|
|
}
|
|
r, err := New(siteFS, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := content.Parse("pages/about.md", []byte("---\ntitle: About\n---\nbody\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := r.Bundle(b, "en", []string{"en"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(out)
|
|
if !strings.Contains(got, `<section class="mine">About</section>`) {
|
|
t.Errorf("the site override should win:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "<!doctype html>") || !strings.Contains(got, `<link rel="canonical"`) {
|
|
t.Errorf("the document should still come from the embedded base:\n%s", got)
|
|
}
|
|
if strings.Contains(got, "<article>") {
|
|
t.Error("the embedded main should have been replaced, not appended")
|
|
}
|
|
}
|
|
|
|
func TestAListingOverrideDoesNotLeakIntoBundlePages(t *testing.T) {
|
|
siteFS := fstest.MapFS{
|
|
"templates/list.html": {Data: []byte(`{{define "main"}}LISTING ONLY{{end}}`)},
|
|
}
|
|
r, err := New(siteFS, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := content.Parse("pages/about.md", []byte("---\ntitle: About\n---\nbody\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := r.Bundle(b, "en", nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(out), "LISTING ONLY") {
|
|
t.Error("a listing override must not reach bundle pages — that is why sets are per kind")
|
|
}
|
|
}
|
|
|
|
func TestSiteStylesheetReplacesTheReferenceOne(t *testing.T) {
|
|
siteFS := fstest.MapFS{"templates/theme.css": {Data: []byte("body{color:rebeccapurple}")}}
|
|
r, err := New(siteFS, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := content.Parse("pages/x.md", []byte("hi\n"))
|
|
out, err := r.Bundle(b, "en", nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(out), "rebeccapurple") {
|
|
t.Error("the site stylesheet should replace the reference one")
|
|
}
|
|
}
|
|
|
|
func TestADeclaredBaseMakesMachineReadableURLsAbsolute(t *testing.T) {
|
|
// A canonical link and an hreflang are read by machines that resolve neither against the page, so both
|
|
// go absolute as soon as the site says where it lives (ADR-0039).
|
|
r, err := New(nil, content.Settings{Base: "https://khosra.example", Title: "Khosra"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := content.Parse("posts/hello.md", []byte("---\ntitle: Hello\n---\nhi\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := r.Bundle(b, "en", []string{"en", "bn"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(out)
|
|
for _, want := range []string{
|
|
`rel="canonical" href="https://khosra.example/posts/hello/"`,
|
|
`hreflang="bn" href="https://khosra.example/bn/posts/hello/"`,
|
|
`property="og:url" content="https://khosra.example/posts/hello/"`,
|
|
`<title>Hello · Khosra</title>`,
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWithoutABaseEverythingStaysRelative(t *testing.T) {
|
|
r, err := New(nil, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := content.Parse("posts/hello.md", []byte("---\ntitle: Hello\n---\nhi\n"))
|
|
out, err := r.Bundle(b, "en", []string{"en"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(out)
|
|
if !strings.Contains(got, `rel="canonical" href="/posts/hello/"`) {
|
|
t.Errorf("a site that declared no base still links to itself:\n%s", got)
|
|
}
|
|
if strings.Contains(got, "og:site_name") {
|
|
t.Error("no declared title means no site_name tag, rather than an empty one")
|
|
}
|
|
}
|
|
|
|
func TestATagListingOffersBothShapesAndLetsTheThemeChoose(t *testing.T) {
|
|
// The engine may not decide that a tag listing looks grouped (ADR-0046). It supplies the partition, because
|
|
// a template cannot group for itself, and the flat list beside it, because choosing is markup.
|
|
r, err := New(nil, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var bundles []content.Bundle
|
|
for _, spec := range []struct{ name, section string }{
|
|
{"posts/essay", "posts"}, {"comics/strip", "comics"}, {"posts/other", "posts"},
|
|
} {
|
|
b, err := content.Parse(spec.name+".md", []byte("---\ntitle: "+spec.name+"\n---\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.Route = b.Key
|
|
bundles = append(bundles, b)
|
|
}
|
|
out, err := r.Tag("", "monsoon", "en", bundles, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The reference theme renders groups, so the sections appear as headings.
|
|
got := string(out)
|
|
for _, want := range []string{"<h2>posts</h2>", "<h2>comics</h2>"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
// A theme preferring a flat list must have one, with each entry able to say where it came from.
|
|
if !strings.Contains(got, "posts/essay") || !strings.Contains(got, "comics/strip") {
|
|
t.Errorf("every entry should be present whichever shape is used:\n%s", got)
|
|
}
|
|
}
|