// Package theme is the first look of gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home.
//
// It draws a header card as inline SVG, a divider and a footer, on top of the
// slot machinery the home realm provides. Everything else on the page comes
// from slots, so this file only decides HOW things are drawn, never WHAT is
// said.
//
// It is a candidate, not the page. Deploying it registers it (see init); the
// authority accepts it in a separate transaction. To change the look in a way
// slots and style knobs cannot express, copy this directory to theme/v1, edit,
// deploy, Accept. The home realm and every slot stay exactly where they are.
//
// Style knobs read from slots (all optional):
//
// style.accent #RRGGBB brass by default
// style.bg #RRGGBB card background
// style.fg #RRGGBB card text
// style.header card | plain | none
//
// Widgets this theme adds to the layout, beside every slot and the home
// realm's computed placeholders:
//
// :header: the card (or a plain heading, or nothing, per style.header)
// :divider: a thin accent rule
// :footer: realm · revision · block · chain · theme · links
package theme
import (
"encoding/base64"
"strconv"
"strings"
"chain/runtime"
"gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home"
)
const (
defaultAccent = "#D9A441"
defaultBg = "#14161C"
defaultFg = "#F4EFE6"
)
// DefaultLayout is what renders when no layout slot is set. content/layout.md
// starts as a copy of it; push that slot to depart from it.
const DefaultLayout = `:header:

:intro:
:links:
## Now
:now:
## Previously
:previously:
## Stack
:stack:
## Before the rabbit hole
:before:
## Numbers
:numbers:
:divider:
:about:
:footer:
`
type clockwork struct{}
var instance = &clockwork{}
// Instance returns the theme object, for a caller that would rather register
// it explicitly than rely on this realm's init having done it.
func Instance() home.Theme { return instance }
// init nominates this theme. Deploying is what registers it; it serves
// nothing until the authority runs Accept("").
func init(cur realm) {
home.Register(cross(cur), instance)
}
// Render draws the profile page and hands every other path to the built-in
// views, so the editor and the system page look the same under any theme.
func (clockwork) Render(path string) string {
if path != "" {
return home.Fallback(path)
}
layout := home.Layout()
if layout == "" {
layout = DefaultLayout
}
f := home.NewFiller()
f.Add("header", header)
f.Add("divider", divider)
f.Add("footer", footer)
return f.Fill(layout)
}
// ---------------------------------------------------------------------------
// Widgets
func header() string {
name := home.Get("name")
if name == "" {
name = home.Path()
}
tagline := home.Get("tagline")
switch home.Style("header", "card") {
case "none":
return ""
case "plain":
out := "# " + name + "\n"
if tagline != "" {
out += "\n_" + tagline + "_\n"
}
return out
}
meta := home.Get("handle")
if meta != "" {
meta += " · "
}
meta += "rev " + strconv.Itoa(home.Revision()) +
" · block " + strconv.FormatInt(runtime.ChainHeight(), 10)
return image(name, card(name, tagline, meta))
}
func divider() string {
accent := color("accent", defaultAccent)
svg := ``
return image("", svg)
}
func footer() string {
theme := home.LivePath()
if theme == "" {
theme = "built-in"
}
return "`" + home.Path() + "` · rev " + strconv.Itoa(home.Revision()) +
" · block " + strconv.FormatInt(runtime.ChainHeight(), 10) +
" · " + runtime.ChainID() +
" · theme `" + theme + "`" +
" · [slots](" + home.Link("slots") + ")" +
" · [edit](" + home.Link("edit") + ")" +
" · [system](" + home.Link("system") + ")"
}
// ---------------------------------------------------------------------------
// SVG
// card draws the header: name, tagline and a meta line on a rounded panel,
// with a gear on the right. Text is clipped, not wrapped, so keep the
// tagline to one line.
func card(name, tagline, meta string) string {
accent := color("accent", defaultAccent)
bg := color("bg", defaultBg)
fg := color("fg", defaultFg)
var b strings.Builder
b.WriteString(``)
return b.String()
}
// image wraps an SVG document as a markdown image with a data: URI. gnoweb
// rejects every data: image except image/svg+xml, and gno.land's CSP allows
// data: in img-src, so this is the one way to draw without hosting anything.
func image(alt, svg string) string {
alt = strings.NewReplacer("[", "", "]", "", "\n", " ").Replace(alt)
return ") + ")"
}
// color returns the style knob when it is a well-formed #RRGGBB, else the
// default. Knobs are admin-written, but a value is still pasted into markup,
// so the shape is checked rather than trusted.
func color(key, fallback string) string {
v := home.Style(key, fallback)
if isHexColor(v) {
return v
}
return fallback
}
func isHexColor(s string) bool {
if len(s) != 7 || s[0] != '#' {
return false
}
for i := 1; i < 7; i++ {
c := s[i]
switch {
case c >= '0' && c <= '9', c >= 'a' && c <= 'f', c >= 'A' && c <= 'F':
default:
return false
}
}
return true
}
// esc escapes text for an SVG element body or attribute.
func esc(s string) string {
return strings.NewReplacer(
"&", "&",
"<", "<",
">", ">",
"\"", """,
"\n", " ",
).Replace(s)
}