Files

1133 lines
31 KiB
Go

package site
import (
"crypto/sha256"
"embed"
hexenc "encoding/hex"
"encoding/json"
"html"
"html/template"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
//go:embed templates/layout.html
var layoutSrc string
//go:embed static
var static embed.FS
const (
SectionStart = "start"
SectionGuides = "guides"
SectionReference = "reference"
otherGroup = "other"
heroAsset = "assets/hero.png"
posterAsset = "assets/demo-poster.png"
shotsDir = "assets/shots/"
scriptSection = "## From a script"
homeLead = "Guides, the language reference and every engine module, generated from the same source as the editor's hover and docs panel."
)
var sectionOrder = []string{SectionStart, SectionGuides, SectionReference}
var sectionTitles = map[string]string{
SectionStart: "Getting started",
SectionGuides: "Guides",
SectionReference: "Reference",
}
func sectionRank(s string) int {
for i, x := range sectionOrder {
if x == s {
return i
}
}
return len(sectionOrder)
}
func sectionTitle(s string) string {
if t, ok := sectionTitles[s]; ok {
return t
}
return s
}
type Options struct {
Playground bool
Repo string
Edit string
Version string
Commit string
Hero string
Poster string
Shots string
Demo bool
PlayVersion string
TryVersion string
Icons []string
Time time.Time
}
type Report struct {
Pages int
Entries int
Broken []string
Outline string
}
type layoutData struct {
AssetV string
Rev string
RevURL string
Root string
Title string
Site string
Playground bool
PlayVersion string
TryVersion string
PlaySize int64
TrySize int64
Repo string
Demo bool
Home bool
Nav template.HTML
Content template.HTML
Toc template.HTML
Pager template.HTML
Footer template.HTML
}
type searchItem struct {
Title string `json:"t"`
URL string `json:"u"`
Summary string `json:"s,omitempty"`
Kind string `json:"k"`
ID string `json:"i"`
Group string `json:"g,omitempty"`
Section string `json:"c,omitempty"`
Icon string `json:"ic,omitempty"`
}
type navGroup struct {
Group
roots []string
}
type builder struct {
exp *Export
out string
assetV string
missing map[string]bool
opt Options
byID map[string]*Entry
kids map[string][]string
groups []navGroup
groupOf map[string]int
own map[string]bool
urls map[string]string
order []string
pos map[string]int
hero bool
poster bool
icons iconSet
conv *converter
layout *template.Template
report Report
}
func Build(exp *Export, out string, opt Options) (Report, error) {
exp.Entries = siteEntries(exp.Entries)
keep := make(map[string]bool, len(exp.Entries))
for _, e := range exp.Entries {
keep[e.ID] = true
}
exp.Nav = pruneNav(exp.Nav, keep)
for i := range exp.Groups {
exp.Groups[i].Nav = pruneNav(exp.Groups[i].Nav, keep)
}
if opt.Edit == "" && opt.Repo != "" {
opt.Edit = strings.TrimRight(opt.Repo, "/") + "/src/branch/main"
}
b := &builder{
exp: exp,
out: out,
opt: opt,
byID: map[string]*Entry{},
missing: map[string]bool{},
kids: map[string][]string{},
groupOf: map[string]int{},
own: map[string]bool{},
urls: map[string]string{},
pos: map[string]int{},
conv: newConverter(),
icons: loadIcons(opt.Icons),
}
layout, err := template.New("layout").Funcs(template.FuncMap{
"icon": func(root, name string) template.HTML { return template.HTML(b.icon(root, name, "")) },
}).Parse(layoutSrc)
if err != nil {
return Report{}, err
}
b.layout = layout
b.assetV = b.assetVersion()
b.hero = isFile(opt.Hero)
b.poster = isFile(opt.Poster)
b.index()
b.route()
b.report.Outline = b.outline()
if err := b.write(); err != nil {
return Report{}, err
}
return b.report, nil
}
func (b *builder) index() {
for i := range b.exp.Entries {
e := &b.exp.Entries[i]
b.byID[e.ID] = e
}
seen := map[string]bool{}
var walk func(gi int, parent string, nodes []Node)
walk = func(gi int, parent string, nodes []Node) {
for _, n := range nodes {
if _, ok := b.byID[n.ID]; !ok || seen[n.ID] {
continue
}
seen[n.ID] = true
b.groupOf[n.ID] = gi
if parent == "" {
b.groups[gi].roots = append(b.groups[gi].roots, n.ID)
} else {
b.kids[parent] = append(b.kids[parent], n.ID)
}
walk(gi, n.ID, n.Children)
}
}
gs := append([]GroupNav(nil), b.exp.Groups...)
sort.SliceStable(gs, func(i, j int) bool {
if ri, rj := sectionRank(gs[i].Section), sectionRank(gs[j].Section); ri != rj {
return ri < rj
}
return gs[i].Order < gs[j].Order
})
for _, g := range gs {
b.groups = append(b.groups, navGroup{Group: g.Group})
walk(len(b.groups)-1, "", g.Nav)
}
var rest []Node
for _, n := range b.exp.Nav {
if !seen[n.ID] {
rest = append(rest, n)
}
}
if len(rest) > 0 {
b.groups = append(b.groups, navGroup{Group: Group{ID: otherGroup, Title: "Other", Section: SectionReference, Order: 1 << 20}})
walk(len(b.groups)-1, "", rest)
}
}
func (b *builder) ownPage(e *Entry) bool {
switch e.Kind {
case KindModule, KindType:
return true
case KindPage:
return e.Parent == "" || titled(e.Body) || len(b.kids[e.ID]) > 0
}
return e.Parent == ""
}
func pagePath(e *Entry) string {
if e.Kind == KindPage {
return cleanPath(e.ID) + ".html"
}
return "ref/" + cleanPath(e.ID) + ".html"
}
func (b *builder) route() {
var walk func(owner string, ids []string)
walk = func(owner string, ids []string) {
for _, id := range ids {
e := b.byID[id]
next := owner
if owner == "" || b.ownPage(e) {
b.own[id] = true
b.urls[id] = pagePath(e)
b.pos[id] = len(b.order)
b.order = append(b.order, id)
next = b.urls[id]
} else {
b.urls[id] = owner + "#" + id
}
walk(next, b.kids[id])
}
}
for _, g := range b.groups {
walk("", g.roots)
}
}
func rootOf(p string) string {
return strings.Repeat("../", strings.Count(p, "/"))
}
func esc(s string) string {
return html.EscapeString(s)
}
var (
mdCode = regexp.MustCompile("`([^`]+)`")
mdStrong = regexp.MustCompile(`\*\*([^*]+)\*\*|__([^_]+)__`)
mdEm = regexp.MustCompile(`(^|[^\w*])\*([^*\s][^*]*)\*|(^|[^\w])_([^_\s][^_]*)_`)
headRe = regexp.MustCompile(`<h([23]) id="([^"]+)"[^>]*>(.*?)</h[23]>`)
kindRe = regexp.MustCompile(`<span class="kind[^"]*">[^<]*</span>`)
tagRe = regexp.MustCompile(`<[^>]+>`)
)
func inlineMD(s string) string {
parts := mdCode.Split(s, -1)
codes := mdCode.FindAllStringSubmatch(s, -1)
var b strings.Builder
for i, part := range parts {
t := esc(part)
t = mdStrong.ReplaceAllString(t, "<strong>$1$2</strong>")
t = mdEm.ReplaceAllString(t, "$1$3<em>$2$4</em>")
b.WriteString(t)
if i < len(codes) {
b.WriteString("<code>" + esc(codes[i][1]) + "</code>")
}
}
return b.String()
}
func plainMD(s string) string {
s = mdCode.ReplaceAllString(s, "$1")
s = mdStrong.ReplaceAllString(s, "$1$2")
return mdEm.ReplaceAllString(s, "$1$3$2$4")
}
func (b *builder) resolve(id string) (string, bool) {
if u, ok := b.urls[id]; ok {
return u, true
}
id = strings.TrimSuffix(strings.TrimSpace(id), "()")
if u, ok := b.urls[id]; ok {
return u, true
}
if real, ok := b.exp.Aliases[id]; ok {
u, ok := b.urls[real]
return u, ok
}
return "", false
}
func (b *builder) code(root, src string) string {
return highlightLinked(src, func(name string) string {
if u, ok := b.resolve(name); ok {
return root + u
}
return ""
})
}
func (b *builder) markdown(root, id, md string) string {
p := &pageCtx{
root: root,
id: id,
play: b.opt.Playground,
resolve: b.resolve,
broken: func(target string) { b.report.Broken = append(b.report.Broken, id+" -> ref:"+target) },
}
out, err := b.conv.render(p, md)
if err != nil {
return "<p>" + esc(err.Error()) + "</p>"
}
return out
}
func kindLabel(e *Entry) string {
if e.Kind == KindPage {
return ""
}
label := e.Kind
if e.Kind == KindType && e.Component != nil {
label = "component"
}
return `<span class="kind kind-` + esc(e.Kind) + `">` + esc(label) + `</span>`
}
func (b *builder) details(c *strings.Builder, e *Entry, root string) {
if e.Signature != "" {
c.WriteString(`<pre class="sig"><code>` + b.code(root, e.Signature) + "</code></pre>\n")
}
body := e.Body
if b.own[e.ID] {
body = stripTitle(body)
}
if strings.TrimSpace(body) != "" {
c.WriteString(b.markdown(root, e.ID, body))
}
if e.Example != "" {
c.WriteString(`<p class="label">Example</p>` + "\n")
c.WriteString(`<pre class="code" data-lang="aes"><code class="lang-aes">` + b.code(root, e.Example) + "</code></pre>\n")
} else if u := b.moduleExample(e); u != "" {
c.WriteString(`<p class="see-example">See the <a href="` + esc(root+u) + `">module example</a> for this function in use.</p>` + "\n")
}
if strings.HasPrefix(e.ID, ScriptsPrefix) && e.Location.Path != "" {
where := e.Location.Path
if e.Location.Line > 0 {
where += ":" + strconv.Itoa(e.Location.Line)
}
c.WriteString(`<p class="where">` + esc(where) + "</p>\n")
}
}
func (b *builder) ref(root, id, text string) string {
if u, ok := b.urls[id]; ok {
ic := ""
if e, ok := b.byID[id]; ok {
ic = b.icon(root, b.entryIcon(e), "")
}
return `<a class="xref" href="` + esc(root+u) + `">` + ic + esc(text) + "</a>"
}
return "<code>" + esc(text) + "</code>"
}
func (b *builder) titleOf(id string) string {
if e, ok := b.byID[id]; ok {
return e.Title
}
return id
}
func (b *builder) componentMeta(e *Entry, root string) string {
ci := e.Component
var parts []string
if ci.Create != "" {
parts = append(parts, `Add it with <span class="create">`+esc(ci.Create)+"</span>.")
}
if len(ci.Requires) > 0 {
links := make([]string, len(ci.Requires))
for i, id := range ci.Requires {
links[i] = b.ref(root, id, b.titleOf(id))
}
parts = append(parts, "Needs "+strings.Join(links, ", ")+" on the same node.")
}
if ci.Unique {
parts = append(parts, "One per scene.")
}
if len(parts) == 0 {
return ""
}
return `<p class="meta">` + strings.Join(parts, " ") + "</p>\n"
}
func (b *builder) fieldTable(c *strings.Builder, e *Entry, root string, heading bool) {
var rows []*Entry
for _, k := range b.kids[e.ID] {
if ke := b.byID[k]; ke.Field != nil {
rows = append(rows, ke)
}
}
if len(rows) == 0 {
return
}
if heading {
c.WriteString(`<h2 id="` + esc(e.ID) + `-fields">Fields</h2>` + "\n")
} else {
c.WriteString(`<p class="label">Fields</p>` + "\n")
}
c.WriteString(`<table class="fields"><thead><tr><th>Name</th><th>Type</th><th>Default</th><th>Notes</th><th>Description</th></tr></thead><tbody>` + "\n")
for _, f := range rows {
fi := f.Field
def := "—"
if fi.Default != "" {
def = "<code>" + esc(fi.Default) + "</code>"
}
var notes []string
if fi.Range != "" {
notes = append(notes, `<span class="range">`+esc(fi.Range)+"</span>")
}
for _, n := range fi.Notes {
notes = append(notes, `<span class="note">`+esc(n)+"</span>")
}
c.WriteString(`<tr id="` + esc(f.ID) + `"><td class="fname"><a class="anchor" href="#` + esc(f.ID) + `">` + b.icon(root, b.entryIcon(f), "") + `<code>` + esc(f.Title) + "</code></a></td>")
c.WriteString("<td><code>" + esc(fi.Type) + "</code></td><td>" + def + "</td><td>" + strings.Join(notes, " ") + "</td>")
c.WriteString("<td>" + b.markdown(root, f.ID, f.Body) + "</td></tr>\n")
}
c.WriteString("</tbody></table>\n")
}
func (b *builder) entryList(c *strings.Builder, ids []string, root string) {
c.WriteString(`<ul class="contents">` + "\n")
for _, id := range ids {
ke, ok := b.byID[id]
if !ok {
continue
}
c.WriteString("<li>" + b.ref(root, id, ke.Title))
if ke.Summary != "" {
c.WriteString(`<span class="summary">` + inlineMD(ke.Summary) + "</span>")
}
c.WriteString("</li>\n")
}
c.WriteString("</ul>\n")
}
func (b *builder) section(c *strings.Builder, id, root string, depth int) {
e := b.byID[id]
h := "h" + strconv.Itoa(min(depth, 4))
c.WriteString(`<section class="member" data-name="` + esc(strings.ToLower(e.Title)) + `">` + "\n")
c.WriteString("<" + h + ` id="` + esc(e.ID) + `"><a class="anchor" href="#` + esc(e.ID) + `">` + b.icon(root, b.entryIcon(e), "") + esc(e.Title) + "</a>" + kindLabel(e) + "</" + h + ">\n")
b.details(c, e, root)
b.fieldTable(c, e, root, false)
b.children(c, e, root, depth+1)
c.WriteString("</section>\n")
}
func (b *builder) children(c *strings.Builder, e *Entry, root string, depth int) {
var pages []string
for _, k := range b.kids[e.ID] {
ke := b.byID[k]
switch {
case ke.Field != nil:
case b.own[k]:
pages = append(pages, k)
default:
b.section(c, k, root, depth)
}
}
if len(pages) == 0 {
return
}
c.WriteString(`<ul class="contents">` + "\n")
for _, k := range pages {
ke := b.byID[k]
c.WriteString(`<li><a href="` + esc(root+b.urls[k]) + `">` + b.icon(root, b.entryIcon(ke), "") + esc(ke.Title) + "</a>" + kindLabel(ke))
if ke.Summary != "" {
c.WriteString(`<span class="summary">` + inlineMD(ke.Summary) + "</span>")
}
c.WriteString("</li>\n")
}
c.WriteString("</ul>\n")
}
func (b *builder) crumbs(e *Entry, root string) string {
var trail []string
for p := e.Parent; p != ""; {
pe, ok := b.byID[p]
if !ok {
break
}
if b.own[p] {
trail = append([]string{`<a href="` + esc(root+b.urls[p]) + `">` + esc(pe.Title) + "</a>"}, trail...)
}
p = pe.Parent
}
if gi, ok := b.groupOf[e.ID]; ok {
g := b.groups[gi]
head := []string{"<span>" + esc(sectionTitle(g.Section)) + "</span>"}
if g.Title != sectionTitle(g.Section) {
head = append(head, "<span>"+esc(g.Title)+"</span>")
}
trail = append(head, trail...)
}
if len(trail) == 0 {
return ""
}
sep := b.icon(root, "ui.chevron_right", "")
if sep == "" {
sep = "/"
}
return `<nav class="crumbs">` + strings.Join(trail, `<span class="sep">`+sep+`</span>`) + "</nav>\n"
}
func (b *builder) tryBlock(c *strings.Builder, e *Entry, root string) {
name := e.ID
if i := strings.LastIndexByte(name, '.'); i >= 0 {
name = name[i+1:]
}
c.WriteString(`<section class="try" data-select="` + esc(e.TrySelect) + `" data-name="` + esc(name) + `" data-scene="` + esc(e.Try) + `">` + "\n")
c.WriteString(`<h2 id="` + esc(e.ID) + `-try">Try it</h2>` + "\n")
c.WriteString(`<p class="try-caption">A live scene with ` + esc(e.Title) + `: edit its fields in the Inspector on the right and the scene restarts with them.</p>` + "\n")
c.WriteString(`<div class="try-stage"><button type="button" class="try-start">` + b.icon(root, "play.start", "") + `Try it</button><span class="try-hint">Runs this scene in the browser, with the Inspector next to it</span></div>` + "\n")
c.WriteString(`<div class="try-bar"><button type="button" class="try-open">` + b.icon(root, "file.save", "") + `Open in editor</button><button type="button" class="try-copy">` + b.icon(root, "edit.copy", "") + `Copy as .aescene</button><button type="button" class="try-reset" disabled>` + b.icon(root, "ui.reset", "") + `Reset</button><span class="try-status" role="status"></span></div>` + "\n")
c.WriteString(b.sceneTree(e.Try, root))
c.WriteString("</section>\n")
}
func (b *builder) content(e *Entry, root string) string {
var c strings.Builder
c.WriteString(b.crumbs(e, root))
c.WriteString(`<h1 id="` + esc(e.ID) + `">` + b.icon(root, b.entryIcon(e), "ic-title") + esc(e.Title) + kindLabel(e) + "</h1>\n")
scripted := ""
if e.Component != nil {
if i := strings.Index(e.Body, scriptSection); i >= 0 {
head := *e
head.Body, scripted = e.Body[:i], e.Body[i:]
e = &head
}
}
if e.Component != nil {
head := *e
if strings.TrimSpace(head.Body) != "" {
c.WriteString(`<div class="lead">` + b.markdown(root, e.ID, head.Body) + "</div>\n")
}
c.WriteString(b.componentMeta(e, root))
head.Body = ""
b.details(&c, &head, root)
} else {
b.details(&c, e, root)
}
if e.Try != "" && b.opt.Playground {
b.tryBlock(&c, e, root)
}
b.fieldTable(&c, e, root, true)
if scripted != "" {
c.WriteString(b.markdown(root, e.ID, scripted))
}
if e.Kind == KindModule && e.Group == groupScriptAPI {
b.apiIndex(&c, e, root)
}
if !b.diagnostics(&c, e, root) {
b.children(&c, e, root, 2)
}
if e.Component != nil && len(e.Component.Events) > 0 {
c.WriteString(`<h2 id="` + esc(e.ID) + `-events">Script events</h2>` + "\n")
c.WriteString("<p>Scripts on a node with this component receive these events from the " + esc(e.Component.Module) + " module.</p>\n")
b.entryList(&c, e.Component.Events, root)
}
if len(e.Related) > 0 {
c.WriteString(`<h2 id="` + esc(e.ID) + `-related">Related guides</h2>` + "\n")
b.entryList(&c, e.Related, root)
}
return c.String()
}
func toc(content string) string {
m := headRe.FindAllStringSubmatch(content, -1)
if len(m) < 2 {
return ""
}
var c strings.Builder
c.WriteString(`<p class="toc-title">On this page</p><ul>`)
for _, x := range m {
text := strings.TrimSpace(html.UnescapeString(tagRe.ReplaceAllString(kindRe.ReplaceAllString(x[3], ""), "")))
c.WriteString(`<li class="l` + x[1] + `"><a href="#` + x[2] + `">` + esc(text) + "</a></li>")
}
c.WriteString("</ul>")
return c.String()
}
func (b *builder) pager(id, root string) string {
i, ok := b.pos[id]
if !ok {
return ""
}
var c strings.Builder
c.WriteString(`<nav class="pager">`)
if i > 0 {
p := b.order[i-1]
c.WriteString(`<a class="prev" href="` + esc(root+b.urls[p]) + `"><span>Previous</span>` + esc(b.byID[p].Title) + "</a>")
} else {
c.WriteString("<span></span>")
}
if i+1 < len(b.order) {
n := b.order[i+1]
c.WriteString(`<a class="next" href="` + esc(root+b.urls[n]) + `"><span>Next</span>` + esc(b.byID[n].Title) + "</a>")
}
c.WriteString("</nav>")
return c.String()
}
func (b *builder) footer(e *Entry, root string) string {
o := b.opt
name := "Aego"
if o.Version != "" {
name += " " + o.Version
}
left := []string{esc(name)}
if o.Commit != "" {
c := esc(o.Commit)
if o.Repo != "" {
c = `<a href="` + esc(strings.TrimRight(o.Repo, "/")+"/commit/"+o.Commit) + `">` + c + "</a>"
}
left = append(left, c)
}
if !o.Time.IsZero() {
left = append(left, "built "+o.Time.UTC().Format("2006-01-02"))
}
var right []string
if e != nil && e.Location.Source != "" && o.Edit != "" {
u := strings.TrimRight(o.Edit, "/") + "/" + e.Location.Source
if e.Location.Line > 0 {
u += "#L" + strconv.Itoa(e.Location.Line)
}
right = append(right, `<a href="`+esc(u)+`">Edit this page</a>`)
}
if o.Playground {
right = append(right, `<a href="`+root+`play/">Playground</a>`)
}
if o.Repo != "" {
right = append(right, `<a href="`+esc(o.Repo)+`">Source</a>`)
}
return `<footer class="foot"><span>` + strings.Join(left, " · ") + `</span><span class="links">` + strings.Join(right, "") + "</span></footer>"
}
func (b *builder) navTree(c *strings.Builder, ids []string, root, current string, open map[string]bool) {
c.WriteString(`<ul class="tree">`)
chev := b.icon(root, "ui.chevron_right", "chev")
for _, id := range ids {
if !b.own[id] {
continue
}
e := b.byID[id]
var sub []string
for _, k := range b.kids[id] {
if b.own[k] {
sub = append(sub, k)
}
}
cls := "item"
aria := ""
if id == current {
cls += " current"
aria = ` aria-current="page"`
}
link := `<a class="` + cls + `"` + aria + ` href="` + esc(root+b.urls[id]) + `">` + b.icon(root, b.entryIcon(e), "") + `<span>` + esc(e.Title) + "</span></a>"
if len(sub) == 0 {
c.WriteString("<li>" + link + "</li>")
continue
}
attr := ""
if open[id] {
attr = " open"
}
c.WriteString(`<li><details` + attr + `><summary>` + link + chev + "</summary>")
b.navTree(c, sub, root, current, open)
c.WriteString("</details></li>")
}
c.WriteString("</ul>")
}
func (b *builder) nav(root, current string) string {
open := map[string]bool{}
for p := current; p != ""; {
open[p] = true
e, ok := b.byID[p]
if !ok {
break
}
p = e.Parent
}
cur := -1
if gi, ok := b.groupOf[current]; ok {
cur = gi
}
chev := b.icon(root, "ui.chevron_right", "chev")
var c strings.Builder
last := ""
for gi, g := range b.groups {
if g.Section != last {
c.WriteString(`<p class="section">` + esc(sectionTitle(g.Section)) + "</p>")
last = g.Section
}
attr := ""
if gi == cur {
attr = ` open data-current="true"`
}
c.WriteString(`<details class="group" data-group="` + esc(g.ID) + `"` + attr + "><summary>" + b.icon(root, g.Icon, "") + "<span>" + esc(g.Title) + "</span>" + chev + "</summary>")
b.navTree(&c, g.roots, root, current, open)
c.WriteString("</details>")
}
return c.String()
}
func (b *builder) firstOf(section string) string {
for _, g := range b.groups {
if g.Section == section && len(g.roots) > 0 {
return b.urls[g.roots[0]]
}
}
return ""
}
func (b *builder) home() string {
var c strings.Builder
c.WriteString(`<section class="hero"><div class="hero-text"><h1>` + esc(b.exp.Title) + "</h1>")
c.WriteString(`<p class="lead">` + esc(homeLead) + `</p><div class="actions">`)
if u := b.firstOf(SectionStart); u != "" {
c.WriteString(`<a class="button primary" href="` + esc(u) + `">Get started</a>`)
}
if b.opt.Playground {
c.WriteString(`<a class="button" href="play/">Open the playground</a>`)
}
if u := b.firstOf(SectionReference); u != "" {
c.WriteString(`<a class="button" href="` + esc(u) + `">Reference</a>`)
}
c.WriteString("</div></div>")
switch {
case b.opt.Demo:
style := ""
if b.poster {
style = ` style="background-image:url(` + posterAsset + `)"`
}
c.WriteString(`<div class="hero-demo"><div class="demo" data-src="demo/"` + style + `><button type="button" class="demo-play" aria-label="Play the demo"><span class="disc">` + b.icon("", "play.start", "") + `</span><small>Play the demo · runs in the browser</small></button></div>`)
c.WriteString(`<p class="demo-caption">A small platformer made with Aego and exported for the web. A/D or arrows to move, Space to jump, J to attack. <a href="demo/">Open full screen</a></p></div>`)
case b.hero:
c.WriteString(`<img class="hero-shot" src="` + heroAsset + `" alt="The Aego editor">`)
}
c.WriteString("</section>\n")
for _, s := range sectionOrder {
var cards []string
for _, g := range b.groups {
if g.Section != s || len(g.roots) == 0 {
continue
}
first := b.byID[g.roots[0]]
card := `<a class="card" href="` + esc(b.urls[g.roots[0]]) + `"><strong>` + b.icon("", g.Icon, "") + esc(g.Title) + "</strong>"
if first.Summary != "" {
card += "<span>" + inlineMD(first.Summary) + "</span>"
}
cards = append(cards, card+"</a>")
}
if len(cards) == 0 {
continue
}
c.WriteString("<h2>" + esc(sectionTitle(s)) + `</h2><div class="cards">` + strings.Join(cards, "") + "</div>\n")
}
return c.String()
}
func (b *builder) emit(rel, title, current, content string, e *Entry) error {
root := rootOf(rel)
data := layoutData{
AssetV: b.assetV,
Rev: b.opt.Commit,
RevURL: b.revURL(),
Root: root,
Title: title,
Site: b.exp.Title,
Playground: b.opt.Playground,
PlayVersion: b.opt.PlayVersion,
TryVersion: b.opt.TryVersion,
PlaySize: fileSize(filepath.Join(b.out, "play", "aego.wasm")),
TrySize: fileSize(filepath.Join(b.out, "try", "aego-docs.wasm")),
Repo: b.opt.Repo,
Demo: b.opt.Demo,
Home: current == "",
Nav: template.HTML(b.nav(root, current)),
Content: template.HTML(content),
Footer: template.HTML(b.footer(e, root)),
}
if current != "" {
data.Toc = template.HTML(toc(content))
data.Pager = template.HTML(b.pager(current, root))
}
var buf strings.Builder
if err := b.layout.Execute(&buf, data); err != nil {
return err
}
dst := filepath.Join(b.out, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
b.report.Pages++
return os.WriteFile(dst, []byte(buf.String()), 0o644)
}
func (b *builder) walkOwn(ids []string, depth int, fn func(id string, depth int)) {
for _, id := range ids {
if !b.own[id] {
continue
}
fn(id, depth)
b.walkOwn(b.kids[id], depth+1, fn)
}
}
func (b *builder) outline() string {
var c strings.Builder
last := ""
for _, g := range b.groups {
if g.Section != last {
c.WriteString(sectionTitle(g.Section) + "\n")
last = g.Section
}
c.WriteString(" " + g.Title + "\n")
b.walkOwn(g.roots, 2, func(id string, depth int) {
c.WriteString(strings.Repeat(" ", depth) + b.byID[id].Title + " -> " + b.urls[id] + "\n")
})
}
return c.String()
}
func (b *builder) llms() string {
var c strings.Builder
c.WriteString("# " + b.exp.Title + "\n\n> " + homeLead + "\n")
last := ""
for _, g := range b.groups {
if g.Section != last {
c.WriteString("\n## " + sectionTitle(g.Section) + "\n")
last = g.Section
}
c.WriteString("\n### " + g.Title + "\n\n")
b.walkOwn(g.roots, 0, func(id string, _ int) {
e := b.byID[id]
line := "- [" + e.Title + "](" + b.urls[id] + ")"
if s := plainMD(e.Summary); s != "" {
line += ": " + s
}
c.WriteString(line + "\n")
})
}
return c.String()
}
func (b *builder) write() error {
if err := os.MkdirAll(b.out, 0o755); err != nil {
return err
}
var search []searchItem
for i := range b.exp.Entries {
e := &b.exp.Entries[i]
u, ok := b.urls[e.ID]
if !ok {
continue
}
b.report.Entries++
item := searchItem{Title: e.Title, URL: u, Summary: plainMD(e.Summary), Kind: e.Kind, ID: e.ID, Icon: b.entryIcon(e)}
if gi, ok := b.groupOf[e.ID]; ok {
item.Group, item.Section = b.groups[gi].Title, b.groups[gi].Section
}
search = append(search, item)
}
for _, id := range b.order {
e := b.byID[id]
rel := b.urls[id]
if err := b.emit(rel, e.Title, id, b.content(e, rootOf(rel)), e); err != nil {
return err
}
}
if err := b.emit("index.html", b.exp.Title, "", b.home(), nil); err != nil {
return err
}
data, err := json.Marshal(search)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(b.out, "search.json"), data, 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(b.out, "llms.txt"), []byte(b.llms()), 0o644); err != nil {
return err
}
return b.assets()
}
func (b *builder) assets() error {
err := fs.WalkDir(static, "static", func(p string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
data, err := static.ReadFile(p)
if err != nil {
return err
}
dst := filepath.Join(b.out, "assets", filepath.FromSlash(strings.TrimPrefix(p, "static/")))
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
return os.WriteFile(dst, data, 0o644)
})
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(b.out, "assets", "icons.svg"), []byte(b.icons.sprite), 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(b.out, "assets", "code.css"), []byte(codeCSS()), 0o644); err != nil {
return err
}
if b.hero {
if err := copyFile(b.opt.Hero, filepath.Join(b.out, filepath.FromSlash(heroAsset))); err != nil {
return err
}
}
if b.poster {
if err := copyFile(b.opt.Poster, filepath.Join(b.out, filepath.FromSlash(posterAsset))); err != nil {
return err
}
}
if b.opt.Shots == "" {
return nil
}
shots, _ := filepath.Glob(filepath.Join(b.opt.Shots, "*.png"))
if len(shots) > 0 {
if err := os.MkdirAll(filepath.Join(b.out, filepath.FromSlash(shotsDir)), 0o755); err != nil {
return err
}
}
for _, f := range shots {
if err := copyFile(f, filepath.Join(b.out, filepath.FromSlash(shotsDir), filepath.Base(f))); err != nil {
return err
}
}
return nil
}
func isFile(path string) bool {
if path == "" {
return false
}
st, err := os.Stat(path)
return err == nil && !st.IsDir()
}
func copyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0o644)
}
func fileSize(path string) int64 {
st, err := os.Stat(path)
if err != nil {
return 0
}
return st.Size()
}
// moduleExample links a Script API function without its own example to its module's runnable example.
func (b *builder) moduleExample(e *Entry) string {
if e.Kind != KindFunction && e.Kind != KindMethod {
return ""
}
m, ok := b.byID[e.Parent]
if !ok || m.Kind != KindModule {
return ""
}
for _, s := range m.Snippets {
if (s.Run || s.Check) && s.Lang == "aes" {
if u, ok := b.urls[m.ID]; ok {
return u + "#example"
}
}
}
return ""
}
const groupScriptAPI = "ref-script"
// apiIndex lists a Script API module's functions and types with their signatures, above the full sections.
func (b *builder) apiIndex(c *strings.Builder, e *Entry, root string) {
var rows []*Entry
consts := 0
for _, k := range b.kids[e.ID] {
ke := b.byID[k]
switch ke.Kind {
case KindFunction, KindMethod, KindType:
rows = append(rows, ke)
default:
consts++
}
}
if len(rows) == 0 {
return
}
c.WriteString(`<h2 id="` + esc(e.ID) + `-index">Functions</h2>` + "\n")
c.WriteString(`<div class="api-index"><label class="member-filter">` + b.icon(root, "ui.search", "") + `<input type="search" placeholder="Filter ` + strconv.Itoa(len(rows)+consts) + ` members of ` + esc(e.Title) + `" aria-label="Filter members"></label>` + "\n")
c.WriteString(`<table class="api">` + "\n")
for _, r := range rows {
sig := r.Signature
if i := strings.IndexByte(sig, '('); i >= 0 && r.Kind != KindType {
sig = sig[i:]
} else {
sig = ""
}
href := "#" + r.ID
if b.own[r.ID] {
href = root + b.urls[r.ID]
}
c.WriteString(`<tr data-name="` + esc(strings.ToLower(r.Title)) + `"><td class="fname"><a href="` + esc(href) + `">` + b.icon(root, b.entryIcon(r), "") + esc(r.Title) + `</a></td><td><code class="lang-aes">` + b.code(root, sig) + `</code></td><td>` + inlineMD(r.Summary) + "</td></tr>\n")
}
c.WriteString("</table>")
if consts > 0 {
c.WriteString(`<p class="api-more">` + strconv.Itoa(consts) + ` constants and enum values are listed below.</p>`)
}
c.WriteString("</div>\n")
}
// siteEntries drops editor-only pages and everything under them.
func siteEntries(all []Entry) []Entry {
parent := make(map[string]string, len(all))
only := map[string]bool{}
for _, e := range all {
parent[e.ID] = e.Parent
if e.EditorOnly {
only[e.ID] = true
}
}
hidden := func(id string) bool {
for n := 0; id != "" && n < 64; n++ {
if only[id] {
return true
}
id = parent[id]
}
return false
}
out := all[:0:0]
for _, e := range all {
if !hidden(e.ID) {
out = append(out, e)
}
}
return out
}
func pruneNav(nodes []Node, keep map[string]bool) []Node {
var out []Node
for _, n := range nodes {
if keep[n.ID] {
n.Children = pruneNav(n.Children, keep)
out = append(out, n)
}
}
return out
}
// assetVersion names the current styles, scripts and icons, so a new build gets new asset URLs.
func (b *builder) assetVersion() string {
h := sha256.New()
_ = fs.WalkDir(static, "static", func(p string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
data, err := static.ReadFile(p)
if err == nil {
h.Write([]byte(p))
h.Write(data)
}
return err
})
h.Write([]byte(b.icons.sprite))
h.Write([]byte(codeCSS()))
return hexenc.EncodeToString(h.Sum(nil))[:12]
}
func (b *builder) revURL() string {
if b.opt.Commit == "" || b.opt.Repo == "" {
return ""
}
return strings.TrimRight(b.opt.Repo, "/") + "/commit/" + b.opt.Commit
}