Files
aego/engine/docs/registry.go
T

739 lines
19 KiB
Go

//go:build !aego_runtime
package docs
import (
"io/fs"
"path"
"slices"
"sort"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"aego/engine/module"
"aego/engine/res"
"aego/engine/settings"
"aego/schema"
"aego/script/abi"
"aego/script/bind"
"aego/script/bind/manifest"
"aego/script/diag"
)
type Kind uint8
const (
Page Kind = iota
Module
Function
Type
Field
Method
Signal
Code
Const
Enum
)
const (
BuiltinID = "aego/builtin"
ComponentsStub = "engine/components"
DiagnosticsID = "diagnostics"
ModulePrefix = "module/"
KeywordPrefix = "keyword/"
StubPrefix = "stub:"
EngineOwner = "engine"
EngineDocsDir = "engine/res/docs"
scriptOwner = "script"
unknownOrder = 1000
)
var kindNames = [...]string{"page", "module", "function", "type", "field", "method", "signal", "code", "const", "enum"}
var kindRank = [...]int{Function: 0, Type: 1, Method: 2, Field: 3, Const: 4, Enum: 5, Signal: 6, Module: 7, Page: 8, Code: 9}
func (k Kind) String() string {
if int(k) < len(kindNames) {
return kindNames[k]
}
return "kind" + strconv.Itoa(int(k))
}
type Param struct {
Name string
Type string
}
type Location struct {
Path string
Line int
Stub string
Symbol string
Source string
}
type Snippet struct {
Lang string
Code string
Run bool
Line int
}
type Entry struct {
ID string
Parent string
Kind Kind
Owner string
Group string
Title string
Summary string
Body string
Signature string
Params []Param
Example string
Location Location
Links []string
Keys []string
Requires []string
Snippets []Snippet
Order int
Field *FieldInfo
Component *ComponentInfo
Related []string
}
type Problem struct {
ID string
Error bool
Message string
}
type Sources struct {
Bindings *manifest.Manifest
Schema *schema.Registry
Modules []module.Info
Docs []module.DocSource
Owners map[string]string
Project string
Scripts []ScriptSymbol
Sections []*module.Section
Settings []settings.Desc
EventOwners map[string]string
}
type pageMeta struct {
group string
ordered bool
}
type Registry struct {
entries []Entry
byID map[string]int
children map[string][]int
keys map[string]int
builtin map[string]bool
mods map[string]module.Info
meta map[string]pageMeta
extra []Group
evOwners map[string]string
problems []Problem
}
func StubPath(module string) string {
return StubPrefix + module + ".aes"
}
func KeywordID(key string) string {
attr := strings.HasPrefix(key, "@")
s := strings.ToLower(strings.TrimPrefix(key, "@"))
if i := strings.IndexByte(s, '<'); i >= 0 {
s = s[:i]
}
s = strings.Join(strings.Fields(s), "-")
if attr {
s += "-attr"
}
return KeywordPrefix + s
}
func Build(m *module.Manifest, b *bind.Registry, s *schema.Registry) (*Registry, error) {
bm, err := manifest.FromRegistry(b, s)
if err != nil {
return nil, err
}
return FromManifest(bm, s, m), nil
}
func SourcesOf(bm *manifest.Manifest, s *schema.Registry, m *module.Manifest) Sources {
src := Sources{Bindings: bm, Schema: s, Docs: []module.DocSource{EngineDocs()}}
if m != nil {
src.Modules, src.Owners = m.Modules, m.ScriptOwners
src.Sections, src.Settings, src.EventOwners = m.Sections, m.Settings, m.EventOwners
src.Docs = append(src.Docs, m.Docs...)
}
return src
}
func FromManifest(bm *manifest.Manifest, s *schema.Registry, m *module.Manifest) *Registry {
return Assemble(SourcesOf(bm, s, m))
}
func EngineDocs() module.DocSource {
sub, err := fs.Sub(res.FS(), "docs")
if err != nil {
sub = res.FS()
}
return module.DocSource{Owner: EngineOwner, FS: sub, Dir: EngineDocsDir}
}
func (r *Registry) Keywords() map[string]string {
out := make(map[string]string, len(r.keys))
for k, i := range r.keys {
out[k] = Plain(r.entries[i].Body)
}
return out
}
func Assemble(src Sources) *Registry {
r := &Registry{
byID: map[string]int{},
children: map[string][]int{},
keys: map[string]int{},
builtin: map[string]bool{},
mods: map[string]module.Info{},
meta: map[string]pageMeta{},
}
for _, mi := range src.Modules {
r.builtin[mi.ID] = mi.Builtin
r.mods[mi.ID] = mi
}
r.add(Entry{ID: BuiltinID, Kind: Module, Owner: scriptOwner, Group: GroupScriptAPI, Order: -1, Title: "Builtin types", Location: Location{Stub: StubPath(BuiltinID)}})
r.add(Entry{ID: DiagnosticsID, Kind: Page, Owner: scriptOwner, Group: GroupDiagnostics, Title: "Diagnostics",
Body: "Every code the compiler, the script runtime and the engine report, with what it means and how to fix it."})
if src.Bindings != nil {
r.bindings(src.Bindings, src.Owners)
r.handles(src.Bindings, src.Owners)
r.hostSignals(src.Bindings, src.Owners)
}
r.evOwners = src.EventOwners
if src.Schema != nil {
r.components(src.Schema, src.Bindings)
}
if src.Bindings != nil {
r.compMethods(src.Bindings)
r.scriptEvents(src.Bindings)
}
r.projectSections(src.Sections)
r.playerSettings(src.Settings)
r.codes()
r.scripts(src.Project, src.Scripts)
for _, d := range src.Docs {
r.pages(d)
}
r.index()
r.related()
return r
}
func (r *Registry) docsOrder(owner string) int {
if mi, ok := r.mods[owner]; ok {
return mi.DocsOrder
}
return unknownOrder
}
func (r *Registry) moduleTitle(owner string) string {
if mi, ok := r.mods[owner]; ok && mi.DisplayName != "" {
return mi.DisplayName
}
return owner
}
func (r *Registry) modulePage(owner string) string {
id := ModulePrefix + owner
if _, ok := r.byID[id]; !ok {
title := r.moduleTitle(owner)
r.add(Entry{ID: id, Kind: Module, Owner: owner, Group: GroupComponents, Order: r.docsOrder(owner), Title: title,
Body: "Components of the " + title + " module."})
}
return id
}
func (r *Registry) handles(m *manifest.Manifest, owners map[string]string) {
for _, h := range m.Handles {
i := strings.LastIndexByte(h.Name, '.')
mod, name := h.Name[:i], h.Name[i+1:]
r.add(Entry{
ID: h.Name, Parent: mod, Kind: Type, Owner: r.ownerOf(mod, owners), Title: name,
Signature: "type " + name, Body: h.Doc,
Location: Location{Stub: StubPath(mod), Symbol: "type:" + name},
})
}
for _, hm := range m.HandleMethods {
i := strings.LastIndexByte(hm.Handle, '.')
mod, name := hm.Handle[:i], hm.Handle[i+1:]
ps := params(hm.Sig, hm.Params)
r.add(Entry{
ID: hm.Handle + "." + hm.Name, Parent: hm.Handle, Kind: Method, Owner: r.ownerOf(mod, owners), Title: name + "." + hm.Name,
Signature: signature("func ("+name+") "+hm.Name, ps, hm.Sig.Result), Params: ps, Body: hm.Doc, Example: hm.Example,
Location: Location{Path: hm.Loc, Stub: StubPath(mod), Symbol: "method:" + name + "." + hm.Name},
})
}
}
func (r *Registry) hostSignals(m *manifest.Manifest, owners map[string]string) {
for _, cs := range m.CompSignals {
ps := params(abi.Signature{Params: cs.Params}, cs.Names)
r.add(Entry{
ID: cs.Comp + "." + cs.Name, Parent: cs.Comp, Kind: Signal, Title: cs.Name,
Signature: signature("signal "+cs.Name, ps, abi.T(abi.Void)), Params: ps, Body: cs.Doc,
Location: Location{Stub: StubPath(ComponentsStub), Symbol: "signal:" + cs.Comp + "." + cs.Name},
})
}
for _, hs := range m.HandleSignals {
i := strings.LastIndexByte(hs.Handle, '.')
mod, short := hs.Handle[:i], hs.Handle[i+1:]
ps := params(abi.Signature{Params: hs.Params}, hs.Names)
r.add(Entry{
ID: hs.Handle + "." + hs.Name, Parent: hs.Handle, Kind: Signal, Owner: r.ownerOf(mod, owners), Title: short + "." + hs.Name,
Signature: signature("signal "+hs.Name, ps, abi.T(abi.Void)), Params: ps, Body: hs.Doc,
Location: Location{Stub: StubPath(mod), Symbol: "signal:" + short + "." + hs.Name},
})
}
}
func (r *Registry) add(e Entry) int {
if i, dup := r.byID[e.ID]; dup {
r.problems = append(r.problems, Problem{ID: e.ID, Error: true, Message: "declared twice, first by " + r.entries[i].Owner})
return i
}
r.byID[e.ID] = len(r.entries)
r.entries = append(r.entries, e)
return len(r.entries) - 1
}
func (r *Registry) known(id string) bool {
_, ok := r.builtin[id]
return ok
}
func (r *Registry) ownerOf(path string, owners map[string]string) string {
if o, ok := owners[path]; ok {
return o
}
first, rest, _ := strings.Cut(path, "/")
if r.known(first) {
return first
}
if first == "aego" {
if seg, _, _ := strings.Cut(rest, "/"); r.known(seg) {
return seg
}
return scriptOwner
}
return ""
}
func params(s abi.Signature, names []string) []Param {
out := make([]Param, len(s.Params))
for i, p := range s.Params {
n := "p" + strconv.Itoa(i)
if i < len(names) {
n = names[i]
}
out[i] = Param{Name: n, Type: p.String()}
}
return out
}
func signature(head string, ps []Param, result abi.TypeRef) string {
var b strings.Builder
b.WriteString(head)
b.WriteByte('(')
for i, p := range ps {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(p.Name + " " + p.Type)
}
b.WriteByte(')')
if result.Kind != abi.Void {
b.WriteString(" " + result.String())
}
return b.String()
}
func (r *Registry) bindings(m *manifest.Manifest, owners map[string]string) {
for _, mod := range m.Modules {
owner := r.ownerOf(mod.Path, owners)
stub := StubPath(mod.Path)
r.add(Entry{ID: mod.Path, Kind: Module, Owner: owner, Group: GroupScriptAPI, Order: r.docsOrder(owner), Title: mod.Path, Location: Location{Stub: stub}})
for _, f := range mod.Funcs {
ps := params(f.Sig, f.Params)
r.add(Entry{
ID: mod.Path + "." + f.Name, Parent: mod.Path, Kind: Function, Owner: owner, Title: f.Name,
Signature: signature("func "+f.Name, ps, f.Sig.Result), Params: ps, Body: f.Doc, Example: f.Example,
Location: Location{Path: f.Loc, Stub: stub, Symbol: "func:" + f.Name},
})
}
for _, c := range mod.Consts {
r.add(Entry{
ID: mod.Path + "." + c.Name, Parent: mod.Path, Kind: Const, Owner: owner, Title: c.Name,
Signature: "const " + c.Name + " " + c.Val.Type.String(), Body: c.Doc,
Location: Location{Stub: stub, Symbol: "const:" + c.Name},
})
}
}
for _, e := range m.Enums {
owner := r.ownerOf(e.Module, owners)
r.add(Entry{
ID: e.Module + "." + e.Name, Parent: e.Module, Kind: Enum, Owner: owner, Title: e.Name,
Signature: "enum " + e.Name, Body: e.Doc,
Location: Location{Stub: StubPath(e.Module), Symbol: "enum:" + e.Name},
})
}
for _, me := range m.Methods {
recv := me.Recv.String()
ps := params(me.Sig, me.Params)
r.add(Entry{
ID: BuiltinID + "." + recv + "." + me.Name, Parent: BuiltinID, Kind: Method, Owner: scriptOwner, Title: recv + "." + me.Name,
Signature: signature("func ("+recv+") "+me.Name, ps, me.Sig.Result), Params: ps, Body: me.Doc, Example: me.Example,
Location: Location{Path: me.Loc, Stub: StubPath(BuiltinID), Symbol: "method:" + recv + "." + me.Name},
})
}
}
func exportName(s string) string {
r, n := utf8.DecodeRuneInString(s)
return string(unicode.ToUpper(r)) + s[n:]
}
func (r *Registry) components(s *schema.Registry, m *manifest.Manifest) {
stub := StubPath(ComponentsStub)
for _, td := range s.Components() {
var mc *manifest.Component
if m != nil {
mc, _ = m.Component(td.FullName)
}
if td.Owner == "" && mc == nil {
continue
}
owner := td.Owner
if owner == "" {
owner = EngineOwner
}
title := td.DisplayName
if title == "" {
title = td.FullName[strings.LastIndexByte(td.FullName, '.')+1:]
}
r.add(Entry{
ID: td.FullName, Parent: r.modulePage(owner), Kind: Type, Owner: owner, Title: title,
Signature: "component " + td.FullName, Body: td.Doc, Component: r.componentInfo(td, owner),
Location: Location{Stub: stub, Symbol: "component:" + td.FullName},
})
base := defaultsOf(td)
for i := range td.Fields {
fd := &td.Fields[i]
if !fd.Flags.Has(schema.FlagEditor) && !td.Scripted(fd) {
continue
}
name := exportName(fd.Name)
sig := "field " + name
info := fieldInfo(td, fd, base, true)
loc := Location{Stub: stub}
if mc != nil {
if f, ok := mc.Field(name); ok {
sig += " " + f.Type.String()
info.Type = f.Type.String()
if f.ReadOnly && !slices.Contains(info.Notes, "read-only") {
sig += " readonly"
info.Notes = append(info.Notes, "read-only in scripts")
}
loc.Symbol = "field:" + td.FullName + "." + name
}
}
r.add(Entry{
ID: td.FullName + "." + name, Parent: td.FullName, Kind: Field, Owner: owner, Title: name,
Signature: sig, Body: fd.Doc, Location: loc, Field: info,
})
}
}
}
func (r *Registry) codes() {
for _, c := range diag.Codes() {
h, _ := diag.Explain(c)
r.add(Entry{
ID: c, Parent: DiagnosticsID, Kind: Code, Owner: scriptOwner, Title: c,
Summary: h.What, Body: h.What + "\n\nFix: " + h.Fix,
})
}
}
func (r *Registry) pages(src module.DocSource) {
var files []string
err := fs.WalkDir(src.FS, ".", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && strings.HasSuffix(p, ".md") {
files = append(files, p)
}
return nil
})
if err != nil {
r.problems = append(r.problems, Problem{ID: src.Owner, Error: true, Message: "docs source cannot be read: " + err.Error()})
return
}
for _, f := range files {
data, err := fs.ReadFile(src.FS, f)
if err != nil {
r.problems = append(r.problems, Problem{ID: src.Owner + ":" + f, Error: true, Message: err.Error()})
continue
}
recs, err := parseRecords(string(data))
if err != nil {
r.problems = append(r.problems, Problem{ID: src.Owner + ":" + f, Error: true, Message: err.Error()})
continue
}
source := ""
if src.Dir != "" {
source = path.Join(src.Dir, f)
}
for _, rec := range recs {
r.page(src.Owner, f, source, rec)
}
}
}
func (r *Registry) page(owner, file, source string, rec record) {
if i, ok := r.byID[rec.id]; ok {
e := &r.entries[i]
if e.Kind == Page {
r.problems = append(r.problems, Problem{ID: rec.id, Error: true, Message: "page declared twice, again in " + owner + ":" + file})
return
}
e.Body = joinBody(e.Body, rec.body)
e.Keys = append(e.Keys, rec.keys...)
if rec.summary != "" {
e.Summary = rec.summary
}
return
}
title := rec.title
if title == "" {
title = heading(rec.body)
}
if title == "" {
title = rec.id
}
r.add(Entry{
ID: rec.id, Parent: rec.parent, Kind: Page, Owner: owner, Group: rec.group, Title: title, Summary: rec.summary,
Body: rec.body, Keys: rec.keys, Requires: rec.requires, Order: rec.order,
Location: Location{Path: owner + ":" + file, Line: rec.line, Source: source},
})
r.meta[rec.id] = pageMeta{group: rec.group, ordered: rec.ordered}
if rec.groupTitle != "" {
r.declareGroup(rec.id, Group{ID: rec.group, Title: rec.groupTitle, Section: SectionGuides, Order: rec.groupOrder})
}
}
func joinBody(a, b string) string {
switch {
case a == "":
return b
case b == "":
return a
}
return a + "\n\n" + b
}
func (r *Registry) index() {
for i := range r.entries {
e := &r.entries[i]
if e.Summary == "" {
e.Summary = summary(e.Body)
}
for _, l := range refs(e.Body) {
if !slices.Contains(e.Links, l) {
e.Links = append(e.Links, l)
}
}
e.Snippets = snippets(e.Body)
if e.Parent != "" {
if _, ok := r.byID[e.Parent]; !ok {
if e.Kind == Page {
r.problems = append(r.problems, Problem{ID: e.ID, Error: true, Message: "parent " + e.Parent + " does not exist"})
}
e.Parent = ""
}
}
if e.Parent != "" {
r.children[e.Parent] = append(r.children[e.Parent], i)
}
for _, k := range e.Keys {
if j, dup := r.keys[k]; dup {
r.problems = append(r.problems, Problem{ID: e.ID, Error: true, Message: "key " + strconv.Quote(k) + " is already used by " + r.entries[j].ID})
continue
}
r.keys[k] = i
}
}
for _, list := range r.children {
sort.SliceStable(list, func(a, b int) bool {
x, y := &r.entries[list[a]], &r.entries[list[b]]
if x.Order != y.Order {
return x.Order < y.Order
}
if x.Kind != y.Kind {
return kindRank[x.Kind] < kindRank[y.Kind]
}
return x.Title < y.Title
})
}
for i := range r.entries {
if r.entries[i].Parent == "" {
r.spread(i, r.entries[i].Group, 0)
}
}
}
func (r *Registry) spread(i int, group string, depth int) {
r.entries[i].Group = group
if depth > 32 {
return
}
for _, c := range r.children[r.entries[i].ID] {
r.spread(c, group, depth+1)
}
}
func (r *Registry) Len() int {
return len(r.entries)
}
func (r *Registry) All() []Entry {
return slices.Clone(r.entries)
}
func (r *Registry) Get(id string) (Entry, bool) {
i, ok := r.byID[id]
if !ok {
return Entry{}, false
}
return r.entries[i], true
}
const KeyPrefix = "key:"
func (r *Registry) Resolve(ref string) (Entry, bool) {
if k, ok := strings.CutPrefix(ref, KeyPrefix); ok {
return r.ByKey(k)
}
return r.Get(strings.TrimPrefix(ref, "ref:"))
}
func (r *Registry) ByKey(key string) (Entry, bool) {
i, ok := r.keys[key]
if !ok {
return Entry{}, false
}
return r.entries[i], true
}
func (r *Registry) Children(id string) []Entry {
list := r.children[id]
out := make([]Entry, len(list))
for k, i := range list {
out[k] = r.entries[i]
}
return out
}
func sortRoots(out []Entry) {
sort.SliceStable(out, func(a, b int) bool {
if out[a].Order != out[b].Order {
return out[a].Order < out[b].Order
}
return out[a].Title < out[b].Title
})
}
func (r *Registry) Roots() []Entry {
var out []Entry
for i := range r.entries {
if r.entries[i].Parent == "" {
out = append(out, r.entries[i])
}
}
sortRoots(out)
return out
}
func (r *Registry) GroupRoots(group string) []Entry {
var out []Entry
for i := range r.entries {
if e := &r.entries[i]; e.Parent == "" && e.Group == group {
out = append(out, *e)
}
}
sortRoots(out)
return out
}
func (r *Registry) Problems() []Problem {
return r.problems
}
func (r *Registry) checkPages(out []Problem) []Problem {
for id, m := range r.meta {
if r.entries[r.byID[id]].Parent != "" {
continue
}
g, ok := r.group(m.group)
switch {
case m.group == "":
out = append(out, Problem{ID: id, Error: true, Message: "root page has no group; add group: one of " + strings.Join(r.guideGroups(), ", ")})
case !ok || g.Section == SectionReference && g.ID != GroupCLI:
out = append(out, Problem{ID: id, Error: true, Message: "group " + m.group + " is not a guide group (" + strings.Join(r.guideGroups(), ", ") + ")"})
}
if !m.ordered {
out = append(out, Problem{ID: id, Error: true, Message: "root page has no order"})
}
}
for _, mi := range r.mods {
if mi.DisplayName == "" {
out = append(out, Problem{ID: ModulePrefix + mi.ID, Error: mi.Builtin, Message: "module has no DisplayName"})
}
}
return out
}
func (r *Registry) Check(strict bool) []Problem {
out := r.checkPages(slices.Clone(r.problems))
for i := range r.entries {
e := &r.entries[i]
for _, l := range e.Links {
if _, ok := r.byID[l]; !ok {
out = append(out, Problem{ID: e.ID, Error: true, Message: "broken link ref:" + l})
}
}
switch {
case (e.Kind == Function || e.Kind == Method) && strings.TrimSpace(e.Body) == "":
out = append(out, Problem{ID: e.ID, Error: strict && e.Owner != "" && !r.builtin[e.Owner], Message: "has no documentation"})
case e.Kind != Module && e.Summary == "":
out = append(out, Problem{ID: e.ID, Message: "empty summary"})
}
}
sort.SliceStable(out, func(a, b int) bool {
if out[a].Error != out[b].Error {
return out[a].Error
}
return out[a].ID < out[b].ID
})
return out
}