refactor server context

This commit is contained in:
Robert Janetzko 2022-04-26 09:24:16 +02:00
parent 5aa0bc5a8d
commit 93e5ebb788
13 changed files with 900 additions and 930 deletions

1
backend/.gitignore vendored
View File

@ -1 +1,2 @@
resources/frontend/* resources/frontend/*
same.json

View File

@ -4,25 +4,16 @@ import (
"embed" "embed"
"flag" "flag"
"fmt" "fmt"
"html/template"
"io/fs"
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
"os/exec"
"runtime" "runtime"
"sort"
"strconv"
"github.com/gorilla/mux"
"github.com/pkg/profile" "github.com/pkg/profile"
"github.com/robertjanetzko/LegendsBrowser2/backend/model" "github.com/robertjanetzko/LegendsBrowser2/backend/model"
"github.com/robertjanetzko/LegendsBrowser2/backend/templates" "github.com/robertjanetzko/LegendsBrowser2/backend/server"
"github.com/robertjanetzko/LegendsBrowser2/backend/util"
) )
var world *model.DfWorld
//go:embed static //go:embed static
var static embed.FS var static embed.FS
@ -31,34 +22,6 @@ func main() {
p := flag.Bool("p", false, "start profiling") p := flag.Bool("p", false, "start profiling")
flag.Parse() flag.Parse()
router := mux.NewRouter().StrictSlash(true)
functions := template.FuncMap{
"json": util.Json,
"check": func(condition bool, v any) any {
if condition {
return v
}
return nil
},
"title": util.Title,
"hf": model.LinkHf,
"getHf": func(id int) *model.HistoricalFigure { return world.HistoricalFigures[id] },
"entity": model.LinkEntity,
"getEntity": func(id int) *model.Entity { return world.Entities[id] },
"site": model.LinkSite,
"getSite": func(id int) *model.Site { return world.Sites[id] },
"region": model.LinkRegion,
"getRegion": func(id int) *model.Region { return world.Regions[id] },
"events": model.NewEventList,
"season": model.Season,
"time": model.Time,
"html": func(value any) template.HTML {
return template.HTML(fmt.Sprint(value))
},
}
t := templates.New(functions)
if len(*f) > 0 { if len(*f) > 0 {
if *p { if *p {
defer profile.Start(profile.ProfilePath(".")).Stop() defer profile.Start(profile.ProfilePath(".")).Stop()
@ -73,137 +36,9 @@ func main() {
os.Exit(1) os.Exit(1)
} }
world = w
runtime.GC() runtime.GC()
RegisterResourcePage(router, "/entity/{id}", t, "entity.html", func(id int) any { return world.Entities[id] }) server.StartServer(w, static)
RegisterResourcePage(router, "/hf/{id}", t, "hf.html", func(id int) any { return world.HistoricalFigures[id] })
RegisterResourcePage(router, "/region/{id}", t, "region.html", func(id int) any { return world.Regions[id] })
RegisterResourcePage(router, "/site/{id}", t, "site.html", func(id int) any { return world.Sites[id] })
RegisterResourcePage(router, "/artifact/{id}", t, "artifact.html", func(id int) any { return world.Artifacts[id] })
RegisterPage(router, "/events", t, "eventTypes.html", func(p Parms) any { return allEventTypes() })
RegisterPage(router, "/events/{type}", t, "eventType.html", func(p Parms) any { return eventsOfType(p["type"]) })
}
spa := spaHandler{staticFS: static, staticPath: "static", indexPath: "index.html"}
router.PathPrefix("/").Handler(spa)
openbrowser("http://localhost:8080")
fmt.Println("Serving at :8080")
http.ListenAndServe(":8080", router)
}
func openbrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
fmt.Println(err)
fmt.Println("navigate to http://localhost:8080 in your browser")
} }
} }
func allEventTypes() []string {
types := make(map[string]bool)
for _, e := range world.HistoricalEvents {
types[e.Details.Type()] = true
}
var list = util.Keys(types)
sort.Strings(list)
return list
}
func eventsOfType(t string) any {
var list []*model.HistoricalEvent
for _, e := range world.HistoricalEvents {
if e.Details.Type() == t {
list = append(list, e)
}
}
sort.Slice(list, func(i, j int) bool { return list[i].Id_ < list[j].Id_ })
return struct {
Type string
Events []*model.HistoricalEvent
}{
Type: t,
Events: list,
}
}
type Parms map[string]string
func RegisterPage(router *mux.Router, path string, templates *templates.Template, template string, accessor func(Parms) any) {
get := func(w http.ResponseWriter, r *http.Request) {
err := templates.Render(w, template, accessor(mux.Vars(r)))
if err != nil {
fmt.Fprintln(w, err)
fmt.Println(err)
}
}
router.HandleFunc(path, get).Methods("GET")
}
func RegisterResourcePage(router *mux.Router, path string, templates *templates.Template, template string, accessor func(int) any) {
RegisterPage(router, path, templates, template, func(params Parms) any {
id, _ := strconv.Atoi(params["id"])
return accessor(id)
})
}
type spaHandler struct {
staticFS embed.FS
staticPath string
indexPath string
}
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// get the absolute path to prevent directory traversal
path := r.URL.Path
// if err != nil {
// // if we failed to get the absolute path respond with a 400 bad request and stop
// http.Error(w, err.Error(), http.StatusBadRequest)
// return
// }
// prepend the path with the path to the static directory
path = h.staticPath + path
fmt.Println(r.URL, "->", path)
_, err := h.staticFS.Open(path)
if os.IsNotExist(err) {
// file does not exist, serve index.html
fmt.Println(path)
index, err := h.staticFS.ReadFile(h.staticPath + "/" + h.indexPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusAccepted)
w.Write(index)
return
} else if err != nil {
// if we got an error (that wasn't that the file doesn't exist) stating the
// file, return a 500 internal server error and stop
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// get the subdirectory of the static dir
statics, err := fs.Sub(h.staticFS, h.staticPath)
// otherwise, use http.FileServer to serve the static dir
http.FileServer(http.FS(statics)).ServeHTTP(w, r)
}

273
backend/model/context.go Normal file
View File

@ -0,0 +1,273 @@
package model
import (
"fmt"
"github.com/iancoleman/strcase"
"github.com/robertjanetzko/LegendsBrowser2/backend/util"
)
type context struct {
world *DfWorld
hfId int
story bool
}
func (c *context) hf(id int) string {
if c.hfId != -1 {
if c.hfId == id {
return c.hfShort(id)
} else {
return c.hfRelated(id, c.hfId)
}
}
if x, ok := c.world.HistoricalFigures[id]; ok {
return fmt.Sprintf(`the %s <a class="hf" href="/hf/%d">%s</a>`, x.Race+util.If(x.Deity, " deity", "")+util.If(x.Force, " force", ""), x.Id(), util.Title(x.Name()))
}
return "UNKNOWN HISTORICAL FIGURE"
}
func (c *context) hfShort(id int) string {
if x, ok := c.world.HistoricalFigures[id]; ok {
return fmt.Sprintf(`<a class="hf" href="/hf/%d">%s</a>`, x.Id(), util.Title(x.FirstName()))
}
return "UNKNOWN HISTORICAL FIGURE"
}
func (c *context) hfRelated(id, to int) string {
if c.hfId != -1 {
if c.hfId == id {
return c.hfShort(id)
} else {
return c.hfRelated(id, c.hfId)
}
}
if x, ok := c.world.HistoricalFigures[id]; ok {
if t, ok := c.world.HistoricalFigures[to]; ok {
if y, ok := util.Find(t.HfLink, func(l *HfLink) bool { return l.Hfid == id }); ok {
return fmt.Sprintf(`%s %s <a class="hf" href="/hf/%d">%s</a>`, t.PossesivePronoun(), y.LinkType, x.Id(), util.Title(x.Name()))
}
}
return fmt.Sprintf(`the %s <a class="hf" href="/hf/%d">%s</a>`, x.Race+util.If(x.Deity, " deity", "")+util.If(x.Force, " force", ""), x.Id(), util.Title(x.Name()))
}
return "UNKNOWN HISTORICAL FIGURE"
}
func (c *context) hfList(ids []int) string {
return andList(util.Map(ids, func(id int) string { return c.hf(id) }))
}
func (c *context) hfListRelated(ids []int, to int) string {
return andList(util.Map(ids, func(id int) string { return c.hfRelated(id, to) }))
}
func (c *context) artifact(id int) string {
if x, ok := c.world.Artifacts[id]; ok {
return fmt.Sprintf(`<a class="artifact" href="/artifact/%d">%s</a>`, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN ARTIFACT"
}
func (c *context) entity(id int) string {
if x, ok := c.world.Entities[id]; ok {
return fmt.Sprintf(`<a class="entity" href="/entity/%d">%s</a>`, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN ENTITY"
}
func (c *context) entityList(ids []int) string {
return andList(util.Map(ids, func(id int) string { return c.entity(id) }))
}
func (c *context) position(entityId, positionId, hfId int) string {
if e, ok := c.world.Entities[entityId]; ok {
if h, ok := c.world.HistoricalFigures[hfId]; ok {
return e.Position(positionId).GenderName(h)
}
}
return "UNKNOWN POSITION"
}
func (c *context) siteCiv(siteCivId, civId int) string {
if siteCivId == civId {
return c.entity(civId)
}
return util.If(siteCivId != -1, c.entity(siteCivId), "") + util.If(civId != -1 && siteCivId != -1, " of ", "") + util.If(civId != -1, c.entity(civId), "")
}
func (c *context) siteStructure(siteId, structureId int, prefix string) string {
if siteId == -1 {
return ""
}
return " " + prefix + " " + util.If(structureId != -1, c.structure(siteId, structureId)+" in ", "") + c.site(siteId, "")
}
func (c *context) site(id int, prefix string) string {
if x, ok := c.world.Sites[id]; ok {
return fmt.Sprintf(`%s <a class="site" href="/site/%d">%s</a>`, prefix, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN SITE"
}
func (c *context) structure(siteId, structureId int) string {
if x, ok := c.world.Sites[siteId]; ok {
if y, ok := x.Structures[structureId]; ok {
return fmt.Sprintf(`<a class="structure" href="/site/%d/structure/%d">%s</a>`, siteId, structureId, util.Title(y.Name()))
}
}
return "UNKNOWN STRUCTURE"
}
func (c *context) property(siteId, propertyId int) string {
if x, ok := c.world.Sites[siteId]; ok {
if y, ok := x.SiteProperties[propertyId]; ok {
if y.StructureId != -1 {
return c.structure(siteId, y.StructureId)
}
return articled(y.Type.String())
}
}
return "UNKNOWN PROPERTY"
}
func (c *context) region(id int) string {
if x, ok := c.world.Regions[id]; ok {
return fmt.Sprintf(`<a class="region" href="/region/%d">%s</a>`, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN REGION"
}
func (c *context) location(siteId int, sitePrefix string, regionId int, regionPrefix string) string {
if siteId != -1 {
return c.site(siteId, sitePrefix)
}
if regionId != -1 {
return regionPrefix + " " + c.region(regionId)
}
return ""
}
func (c *context) place(structureId, siteId int, sitePrefix string, regionId int, regionPrefix string) string {
if siteId != -1 {
return c.siteStructure(siteId, structureId, sitePrefix)
}
if regionId != -1 {
return regionPrefix + " " + c.region(regionId)
}
return ""
}
func (c *context) mountain(id int) string {
if x, ok := c.world.MountainPeaks[id]; ok {
return fmt.Sprintf(`<a class="mountain" href="/site/%d">%s</a>`, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN MOUNTAIN"
}
func (c *context) identity(id int) string {
if x, ok := c.world.Identities[id]; ok {
return fmt.Sprintf(`<a class="identity" href="/region/%d">%s</a>`, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN IDENTITY"
}
func (c *context) fullIdentity(id int) string {
if x, ok := c.world.Identities[id]; ok {
return fmt.Sprintf(`&quot;the %s <a class="identity" href="/region/%d">%s</a> of %s&quot;`, x.Profession.String(), x.Id(), util.Title(x.Name()), c.entity(x.EntityId))
}
return "UNKNOWN IDENTITY"
}
func (c *context) danceForm(id int) string {
if x, ok := c.world.DanceForms[id]; ok {
return fmt.Sprintf(`<a class="artform" href="/danceForm/%d">%s</a>`, id, util.Title(x.Name()))
}
return "UNKNOWN DANCE FORM"
}
func (c *context) musicalForm(id int) string {
if x, ok := c.world.MusicalForms[id]; ok {
return fmt.Sprintf(`<a class="artform" href="/musicalForm/%d">%s</a>`, id, util.Title(x.Name()))
}
return "UNKNOWN MUSICAL FORM"
}
func (c *context) poeticForm(id int) string {
if x, ok := c.world.PoeticForms[id]; ok {
return fmt.Sprintf(`<a class="artform" href="/poeticForm/%d">%s</a>`, id, util.Title(x.Name()))
}
return "UNKNOWN POETIC FORM"
}
func (c *context) worldConstruction(id int) string {
if x, ok := c.world.WorldConstructions[id]; ok {
return fmt.Sprintf(`<a class="artform" href="/wc/%d">%s</a>`, id, util.Title(x.Name()))
}
return "UNKNOWN WORLD CONSTRUCTION"
}
func (c *context) writtenContent(id int) string {
if x, ok := c.world.WrittenContents[id]; ok {
return fmt.Sprintf(`<a class="artform" href="/writtenContent/%d">%s</a>`, id, util.Title(x.Name()))
}
return "UNKNOWN WORLD CONSTRUCTION"
}
func (c *context) feature(x *Feature) string {
switch x.Type {
case FeatureType_DancePerformance:
return "a perfomance of " + c.danceForm(x.Reference)
case FeatureType_Images:
if x.Reference != -1 {
return "images of " + c.hf(x.Reference)
}
return "images"
case FeatureType_MusicalPerformance:
return "a perfomance of " + c.musicalForm(x.Reference)
case FeatureType_PoetryRecital:
return "a recital of " + c.poeticForm(x.Reference)
case FeatureType_Storytelling:
if x.Reference != -1 {
if e, ok := c.world.HistoricalEvents[x.Reference]; ok {
return "a telling of the story of " + e.Details.Html(&context{story: true}) + " in " + Time(e.Year, e.Seconds72)
}
}
return "a story recital"
default:
return strcase.ToDelimited(x.Type.String(), ' ')
}
}
func (c *context) schedule(x *Schedule) string {
switch x.Type {
case ScheduleType_DancePerformance:
return "a perfomance of " + c.danceForm(x.Reference)
case ScheduleType_MusicalPerformance:
return "a perfomance of " + c.musicalForm(x.Reference)
case ScheduleType_PoetryRecital:
return "a recital of " + c.poeticForm(x.Reference)
case ScheduleType_Storytelling:
if x.Reference != -1 {
if e, ok := c.world.HistoricalEvents[x.Reference]; ok {
return "the story of " + e.Details.Html(&context{story: true}) + " in " + Time(e.Year, e.Seconds72)
}
}
return "a story recital"
default:
return strcase.ToDelimited(x.Type.String(), ' ')
}
}
func (c *context) pronoun(id int) string {
if x, ok := c.world.HistoricalFigures[id]; ok {
return x.Pronoun()
}
return "he"
}
func (c *context) posessivePronoun(id int) string {
if x, ok := c.world.HistoricalFigures[id]; ok {
return x.PossesivePronoun()
}
return "his"
}

View File

@ -0,0 +1,49 @@
package model
import (
"fmt"
)
type HistoricalEventDetails interface {
RelatedToEntity(int) bool
RelatedToHf(int) bool
RelatedToArtifact(int) bool
RelatedToSite(int) bool
RelatedToRegion(int) bool
Html(*context) string
Type() string
}
type HistoricalEventCollectionDetails interface {
}
type EventList struct {
Events []*HistoricalEvent
Context *context
}
func NewEventList(world *DfWorld, obj any) *EventList {
el := EventList{
Context: &context{hfId: -1},
}
switch x := obj.(type) {
case *Entity:
el.Events = world.EventsMatching(func(d HistoricalEventDetails) bool { return d.RelatedToEntity(x.Id()) })
case *HistoricalFigure:
el.Context.hfId = x.Id()
el.Events = world.EventsMatching(func(d HistoricalEventDetails) bool { return d.RelatedToHf(x.Id()) })
case *Artifact:
el.Events = world.EventsMatching(func(d HistoricalEventDetails) bool { return d.RelatedToArtifact(x.Id()) })
case *Site:
el.Events = world.EventsMatching(func(d HistoricalEventDetails) bool { return d.RelatedToSite(x.Id()) })
case *Region:
el.Events = world.EventsMatching(func(d HistoricalEventDetails) bool { return d.RelatedToRegion(x.Id()) })
case []*HistoricalEvent:
el.Events = x
default:
fmt.Printf("unknown type %T\n", obj)
}
return &el
}

File diff suppressed because it is too large Load Diff

View File

@ -2,15 +2,52 @@ package model
import ( import (
"fmt" "fmt"
"html/template"
"regexp"
"sort" "sort"
"strings" "strings"
"github.com/iancoleman/strcase"
"github.com/robertjanetzko/LegendsBrowser2/backend/util" "github.com/robertjanetzko/LegendsBrowser2/backend/util"
) )
func (w *DfWorld) AllEventTypes() []string {
types := make(map[string]bool)
for _, e := range w.HistoricalEvents {
types[e.Details.Type()] = true
}
var list = util.Keys(types)
sort.Strings(list)
return list
}
func (w *DfWorld) EventsOfType(t string) any {
var list []*HistoricalEvent
for _, e := range w.HistoricalEvents {
if e.Details.Type() == t {
list = append(list, e)
}
}
sort.Slice(list, func(i, j int) bool { return list[i].Id_ < list[j].Id_ })
return struct {
Type string
Events []*HistoricalEvent
}{
Type: t,
Events: list,
}
}
func (w *DfWorld) EventsMatching(f func(HistoricalEventDetails) bool) []*HistoricalEvent {
var list []*HistoricalEvent
for _, e := range w.HistoricalEvents {
if f(e.Details) {
list = append(list, e)
}
}
sort.Slice(list, func(a, b int) bool { return list[a].Id_ < list[b].Id_ })
return list
}
func (e *Entity) Position(id int) *EntityPosition { func (e *Entity) Position(id int) *EntityPosition {
for _, p := range e.EntityPosition { for _, p := range e.EntityPosition {
if p.Id_ == id { if p.Id_ == id {
@ -56,434 +93,30 @@ func (hf *HistoricalFigure) FirstName() string {
return strings.Split(hf.Name_, " ")[0] return strings.Split(hf.Name_, " ")[0]
} }
func (x *Honor) Requirement() string {
var list []string
if x.RequiresAnyMeleeOrRangedSkill {
list = append(list, "attaining sufficent skill with a weapon or technique")
}
if x.RequiredSkill != HonorRequiredSkill_Unknown {
list = append(list, "attaining enough skill with the "+x.RequiredSkill.String())
}
if x.RequiredBattles == 1 {
list = append(list, "serving in combat")
}
if x.RequiredBattles > 1 {
list = append(list, fmt.Sprintf("participating in %d battles", x.RequiredBattles))
}
if x.RequiredYears >= 1 {
list = append(list, fmt.Sprintf("%d years of membership", x.RequiredYears))
}
if x.RequiredKills >= 1 {
list = append(list, fmt.Sprintf("slaying %d enemies", x.RequiredKills))
}
return " after " + andList(list)
}
func (w *WrittenContent) Name() string { func (w *WrittenContent) Name() string {
return w.Title return w.Title
} }
type HistoricalEventDetails interface {
RelatedToEntity(int) bool
RelatedToHf(int) bool
RelatedToArtifact(int) bool
RelatedToSite(int) bool
RelatedToRegion(int) bool
Html(*context) string
Type() string
}
type HistoricalEventCollectionDetails interface {
}
type EventList struct {
Events []*HistoricalEvent
Context *context
}
func filter(f func(HistoricalEventDetails) bool) []*HistoricalEvent {
var list []*HistoricalEvent
for _, e := range world.HistoricalEvents {
if f(e.Details) {
list = append(list, e)
}
}
sort.Slice(list, func(a, b int) bool { return list[a].Id_ < list[b].Id_ })
return list
}
func NewEventList(obj any) *EventList {
el := EventList{
Context: &context{HfId: -1},
}
switch x := obj.(type) {
case *Entity:
el.Events = filter(func(d HistoricalEventDetails) bool { return d.RelatedToEntity(x.Id()) })
case *HistoricalFigure:
el.Context.HfId = x.Id()
el.Events = filter(func(d HistoricalEventDetails) bool { return d.RelatedToHf(x.Id()) })
case *Artifact:
el.Events = filter(func(d HistoricalEventDetails) bool { return d.RelatedToArtifact(x.Id()) })
case *Site:
el.Events = filter(func(d HistoricalEventDetails) bool { return d.RelatedToSite(x.Id()) })
case *Region:
el.Events = filter(func(d HistoricalEventDetails) bool { return d.RelatedToRegion(x.Id()) })
case []*HistoricalEvent:
el.Events = x
default:
fmt.Printf("unknown type %T\n", obj)
}
return &el
}
type context struct {
HfId int
Story bool
}
func (c *context) hf(id int) string {
if c.HfId != -1 {
if c.HfId == id {
return hfShort(id)
} else {
return hfRelated(id, c.HfId)
}
}
return hf(id)
}
func (c *context) hfShort(id int) string {
return hfShort(id)
}
func (c *context) hfRelated(id, to int) string {
if c.HfId != -1 {
if c.HfId == id {
return hfShort(id)
} else {
return hfRelated(id, c.HfId)
}
}
return hfRelated(id, to)
}
func (c *context) hfList(ids []int) string {
return andList(util.Map(ids, func(id int) string { return c.hf(id) }))
}
func (c *context) hfListRelated(ids []int, to int) string {
return andList(util.Map(ids, func(id int) string { return c.hfRelated(id, to) }))
}
func containsInt(list []int, id int) bool {
for _, v := range list {
if v == id {
return true
}
}
return false
}
var world *DfWorld
func andList(list []string) string {
if len(list) > 1 {
return strings.Join(list[:len(list)-1], ", ") + " and " + list[len(list)-1]
}
return strings.Join(list, ", ")
}
func articled(s string) string {
if ok, _ := regexp.MatchString("^([aeio]|un|ul).*", s); ok {
return "an " + s
}
return "a " + s
}
func artifact(id int) string {
if x, ok := world.Artifacts[id]; ok {
return fmt.Sprintf(`<a class="artifact" href="/artifact/%d">%s</a>`, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN ARTIFACT"
}
func entity(id int) string {
if x, ok := world.Entities[id]; ok {
return fmt.Sprintf(`<a class="entity" href="/entity/%d">%s</a>`, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN ENTITY"
}
func entityList(ids []int) string {
return andList(util.Map(ids, entity))
}
func position(entityId, positionId, hfId int) string {
if e, ok := world.Entities[entityId]; ok {
if h, ok := world.HistoricalFigures[hfId]; ok {
return e.Position(positionId).GenderName(h)
}
}
return "UNKNOWN POSITION"
}
func siteCiv(siteCivId, civId int) string {
if siteCivId == civId {
return entity(civId)
}
return util.If(siteCivId != -1, entity(siteCivId), "") + util.If(civId != -1 && siteCivId != -1, " of ", "") + util.If(civId != -1, entity(civId), "")
}
func siteStructure(siteId, structureId int, prefix string) string {
if siteId == -1 {
return ""
}
return " " + prefix + " " + util.If(structureId != -1, structure(siteId, structureId)+" in ", "") + site(siteId, "")
}
func hf(id int) string {
if x, ok := world.HistoricalFigures[id]; ok {
return fmt.Sprintf(`the %s <a class="hf" href="/hf/%d">%s</a>`, x.Race+util.If(x.Deity, " deity", "")+util.If(x.Force, " force", ""), x.Id(), util.Title(x.Name()))
}
return "UNKNOWN HISTORICAL FIGURE"
}
func hfShort(id int) string {
if x, ok := world.HistoricalFigures[id]; ok {
return fmt.Sprintf(`<a class="hf" href="/hf/%d">%s</a>`, x.Id(), util.Title(x.FirstName()))
}
return "UNKNOWN HISTORICAL FIGURE"
}
func hfRelated(id, to int) string {
if x, ok := world.HistoricalFigures[id]; ok {
if t, ok := world.HistoricalFigures[to]; ok {
if y, ok := util.Find(t.HfLink, func(l *HfLink) bool { return l.Hfid == id }); ok {
return fmt.Sprintf(`%s %s <a class="hf" href="/hf/%d">%s</a>`, t.PossesivePronoun(), y.LinkType, x.Id(), util.Title(x.Name()))
}
}
return fmt.Sprintf(`the %s <a class="hf" href="/hf/%d">%s</a>`, x.Race+util.If(x.Deity, " deity", "")+util.If(x.Force, " force", ""), x.Id(), util.Title(x.Name()))
}
return "UNKNOWN HISTORICAL FIGURE"
}
func hfLink(id, to int) string {
if x, ok := world.HistoricalFigures[id]; ok {
if y, ok := util.Find(x.HfLink, func(l *HfLink) bool { return l.Hfid == to }); ok {
return y.LinkType.String()
}
}
return "UNKNOWN LINK"
}
func pronoun(id int) string {
if x, ok := world.HistoricalFigures[id]; ok {
return x.Pronoun()
}
return "he"
}
func posessivePronoun(id int) string {
if x, ok := world.HistoricalFigures[id]; ok {
return x.PossesivePronoun()
}
return "his"
}
func site(id int, prefix string) string {
if x, ok := world.Sites[id]; ok {
return fmt.Sprintf(`%s <a class="site" href="/site/%d">%s</a>`, prefix, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN SITE"
}
func structure(siteId, structureId int) string {
if x, ok := world.Sites[siteId]; ok {
if y, ok := x.Structures[structureId]; ok {
return fmt.Sprintf(`<a class="structure" href="/site/%d/structure/%d">%s</a>`, siteId, structureId, util.Title(y.Name()))
}
}
return "UNKNOWN STRUCTURE"
}
func property(siteId, propertyId int) string {
if x, ok := world.Sites[siteId]; ok {
if y, ok := x.SiteProperties[propertyId]; ok {
if y.StructureId != -1 {
return structure(siteId, y.StructureId)
}
return articled(y.Type.String())
}
}
return "UNKNOWN PROPERTY"
}
func region(id int) string {
if x, ok := world.Regions[id]; ok {
return fmt.Sprintf(`<a class="region" href="/region/%d">%s</a>`, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN REGION"
}
func location(siteId int, sitePrefix string, regionId int, regionPrefix string) string {
if siteId != -1 {
return site(siteId, sitePrefix)
}
if regionId != -1 {
return regionPrefix + " " + region(regionId)
}
return ""
}
func place(structureId, siteId int, sitePrefix string, regionId int, regionPrefix string) string {
if siteId != -1 {
return siteStructure(siteId, structureId, sitePrefix)
}
if regionId != -1 {
return regionPrefix + " " + region(regionId)
}
return ""
}
func mountain(id int) string {
if x, ok := world.MountainPeaks[id]; ok {
return fmt.Sprintf(`<a class="mountain" href="/site/%d">%s</a>`, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN MOUNTAIN"
}
func identity(id int) string {
if x, ok := world.Identities[id]; ok {
return fmt.Sprintf(`<a class="identity" href="/region/%d">%s</a>`, x.Id(), util.Title(x.Name()))
}
return "UNKNOWN IDENTITY"
}
func fullIdentity(id int) string {
if x, ok := world.Identities[id]; ok {
return fmt.Sprintf(`&quot;the %s <a class="identity" href="/region/%d">%s</a> of %s&quot;`, x.Profession.String(), x.Id(), util.Title(x.Name()), entity(x.EntityId))
}
return "UNKNOWN IDENTITY"
}
func feature(x *Feature) string {
switch x.Type {
case FeatureType_DancePerformance:
return "a perfomance of " + danceForm(x.Reference)
case FeatureType_Images:
if x.Reference != -1 {
return "images of " + hf(x.Reference)
}
return "images"
case FeatureType_MusicalPerformance:
return "a perfomance of " + musicalForm(x.Reference)
case FeatureType_PoetryRecital:
return "a recital of " + poeticForm(x.Reference)
case FeatureType_Storytelling:
if x.Reference != -1 {
if e, ok := world.HistoricalEvents[x.Reference]; ok {
return "a telling of the story of " + e.Details.Html(&context{Story: true}) + " in " + Time(e.Year, e.Seconds72)
}
}
return "a story recital"
default:
return strcase.ToDelimited(x.Type.String(), ' ')
}
}
func schedule(x *Schedule) string {
switch x.Type {
case ScheduleType_DancePerformance:
return "a perfomance of " + danceForm(x.Reference)
case ScheduleType_MusicalPerformance:
return "a perfomance of " + musicalForm(x.Reference)
case ScheduleType_PoetryRecital:
return "a recital of " + poeticForm(x.Reference)
case ScheduleType_Storytelling:
if x.Reference != -1 {
if e, ok := world.HistoricalEvents[x.Reference]; ok {
return "the story of " + e.Details.Html(&context{Story: true}) + " in " + Time(e.Year, e.Seconds72)
}
}
return "a story recital"
default:
return strcase.ToDelimited(x.Type.String(), ' ')
}
}
func ShortTime(year, seconds int) string {
if year == -1 {
return "a time before time"
}
return fmt.Sprintf("%d", year)
}
func Time(year, seconds int) string {
if year == -1 {
return "a time before time"
}
if seconds == -1 {
return fmt.Sprintf("%d", year)
}
return fmt.Sprintf("%s of %d", Season(seconds), year)
}
func Season(seconds int) string {
r := ""
month := seconds % 100800
if month <= 33600 {
r += "early "
} else if month <= 67200 {
r += "mid"
} else if month <= 100800 {
r += "late "
}
season := seconds % 403200
if season < 100800 {
r += "spring"
} else if season < 201600 {
r += "summer"
} else if season < 302400 {
r += "autumn"
} else if season < 403200 {
r += "winter"
}
return r
}
func danceForm(id int) string {
if x, ok := world.DanceForms[id]; ok {
return fmt.Sprintf(`<a class="artform" href="/danceForm/%d">%s</a>`, id, util.Title(x.Name()))
}
return "UNKNOWN DANCE FORM"
}
func musicalForm(id int) string {
if x, ok := world.MusicalForms[id]; ok {
return fmt.Sprintf(`<a class="artform" href="/musicalForm/%d">%s</a>`, id, util.Title(x.Name()))
}
return "UNKNOWN MUSICAL FORM"
}
func poeticForm(id int) string {
if x, ok := world.PoeticForms[id]; ok {
return fmt.Sprintf(`<a class="artform" href="/poeticForm/%d">%s</a>`, id, util.Title(x.Name()))
}
return "UNKNOWN POETIC FORM"
}
func worldConstruction(id int) string {
if x, ok := world.WorldConstructions[id]; ok {
return fmt.Sprintf(`<a class="artform" href="/wc/%d">%s</a>`, id, util.Title(x.Name()))
}
return "UNKNOWN WORLD CONSTRUCTION"
}
func writtenContent(id int) string {
if x, ok := world.WrittenContents[id]; ok {
return fmt.Sprintf(`<a class="artform" href="/writtenContent/%d">%s</a>`, id, util.Title(x.Name()))
}
return "UNKNOWN WORLD CONSTRUCTION"
}
var LinkHf = func(id int) template.HTML { return template.HTML(hf(id)) }
var LinkEntity = func(id int) template.HTML { return template.HTML(entity(id)) }
var LinkSite = func(id int) template.HTML { return template.HTML(site(id, "")) }
var LinkRegion = func(id int) template.HTML { return template.HTML(region(id)) }
func equipmentLevel(level int) string {
switch level {
case 1:
return "well-crafted"
case 2:
return "finely-crafted"
case 3:
return "superior quality"
case 4:
return "exceptional"
case 5:
return "masterwork"
}
return ""
}

View File

@ -0,0 +1,94 @@
package model
import (
"fmt"
"html/template"
"regexp"
"strings"
)
var LinkHf = func(w *DfWorld, id int) template.HTML { return template.HTML((&context{world: w}).hf(id)) }
var LinkEntity = func(w *DfWorld, id int) template.HTML { return template.HTML((&context{world: w}).entity(id)) }
var LinkSite = func(w *DfWorld, id int) template.HTML { return template.HTML((&context{world: w}).site(id, "")) }
var LinkRegion = func(w *DfWorld, id int) template.HTML { return template.HTML((&context{world: w}).region(id)) }
func andList(list []string) string {
if len(list) > 1 {
return strings.Join(list[:len(list)-1], ", ") + " and " + list[len(list)-1]
}
return strings.Join(list, ", ")
}
func articled(s string) string {
if ok, _ := regexp.MatchString("^([aeio]|un|ul).*", s); ok {
return "an " + s
}
return "a " + s
}
func ShortTime(year, seconds int) string {
if year == -1 {
return "a time before time"
}
return fmt.Sprintf("%d", year)
}
func Time(year, seconds int) string {
if year == -1 {
return "a time before time"
}
if seconds == -1 {
return fmt.Sprintf("%d", year)
}
return fmt.Sprintf("%s of %d", Season(seconds), year)
}
func Season(seconds int) string {
r := ""
month := seconds % 100800
if month <= 33600 {
r += "early "
} else if month <= 67200 {
r += "mid"
} else if month <= 100800 {
r += "late "
}
season := seconds % 403200
if season < 100800 {
r += "spring"
} else if season < 201600 {
r += "summer"
} else if season < 302400 {
r += "autumn"
} else if season < 403200 {
r += "winter"
}
return r
}
func equipmentLevel(level int) string {
switch level {
case 1:
return "well-crafted"
case 2:
return "finely-crafted"
case 3:
return "superior quality"
case 4:
return "exceptional"
case 5:
return "masterwork"
}
return ""
}
func containsInt(list []int, id int) bool {
for _, v := range list {
if v == id {
return true
}
}
return false
}

View File

@ -70,6 +70,8 @@ func Parse(file string) (*DfWorld, error) {
} }
defer xmlFile.Close() defer xmlFile.Close()
world := &DfWorld{}
BaseLoop: BaseLoop:
for { for {
t, n, err := p.Token() t, n, err := p.Token()

View File

@ -1,40 +0,0 @@
{
"HistoricalEventAddHfEntityLink": {
"LinkType": "Link"
},
"HistoricalEventAddHfSiteLink": {
"Site": "SiteId"
},
"HistoricalEventAgreementMade": {
"Destination": "SiteId",
"Site": "SiteId",
"Source": "SiteId"
},
"HistoricalEventAgreementRejected": {
"Destination": "SiteId",
"Site": "SiteId",
"Source": "SiteId"
},
"HistoricalEventDiplomatLost": {
"Entity": "SiteId",
"Involved": "SiteId",
"Site": "SiteId"
},
"HistoricalEventHfDied": {
"DeathCause": "Cause",
"ShooterItemSubtype": "Cause",
"ShooterItemType": "Cause"
},
"HistoricalEventPeaceAccepted": {
"Site": "SiteId"
},
"HistoricalEventPeaceRejected": {
"Site": "SiteId"
},
"HistoricalEventRemoveHfEntityLink": {
"LinkType": "Link"
},
"HistoricalEventRemoveHfSiteLink": {
"Site": "SiteId"
}
}

View File

@ -1,39 +1,30 @@
package server package server
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/robertjanetzko/LegendsBrowser2/backend/model"
) )
type Info struct { type Parms map[string]string
Id int `json:"id"`
Name string `json:"name"`
}
func RegisterResource[T model.Named](router *mux.Router, resourceName string, resources map[int]T) {
list := func(w http.ResponseWriter, r *http.Request) {
values := make([]Info, 0, len(resources))
for _, v := range resources {
values = append(values, Info{Id: v.Id(), Name: v.Name()})
}
json.NewEncoder(w).Encode(values)
}
func (srv *DfServer) RegisterPage(path string, template string, accessor func(Parms) any) {
get := func(w http.ResponseWriter, r *http.Request) { get := func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(mux.Vars(r)["id"]) err := srv.templates.Render(w, template, accessor(mux.Vars(r)))
if err != nil { if err != nil {
fmt.Fprintln(w, err)
fmt.Println(err) fmt.Println(err)
} }
json.NewEncoder(w).Encode(resources[id])
} }
router.HandleFunc(fmt.Sprintf("/api/%s", resourceName), list).Methods("GET") srv.router.HandleFunc(path, get).Methods("GET")
router.HandleFunc(fmt.Sprintf("/api/%s/{id}", resourceName), get).Methods("GET") }
func (srv *DfServer) RegisterResourcePage(path string, template string, accessor func(int) any) {
srv.RegisterPage(path, template, func(params Parms) any {
id, _ := strconv.Atoi(params["id"])
return accessor(id)
})
} }

90
backend/server/server.go Normal file
View File

@ -0,0 +1,90 @@
package server
import (
"embed"
"fmt"
"io/fs"
"net/http"
"os"
"github.com/gorilla/mux"
"github.com/robertjanetzko/LegendsBrowser2/backend/model"
"github.com/robertjanetzko/LegendsBrowser2/backend/templates"
)
type DfServer struct {
router *mux.Router
templates *templates.Template
world *model.DfWorld
}
func StartServer(world *model.DfWorld, static embed.FS) {
srv := &DfServer{
router: mux.NewRouter().StrictSlash(true),
world: world,
}
srv.LoadTemplates()
srv.RegisterResourcePage("/entity/{id}", "entity.html", func(id int) any { return srv.world.Entities[id] })
srv.RegisterResourcePage("/hf/{id}", "hf.html", func(id int) any { return srv.world.HistoricalFigures[id] })
srv.RegisterResourcePage("/region/{id}", "region.html", func(id int) any { return srv.world.Regions[id] })
srv.RegisterResourcePage("/site/{id}", "site.html", func(id int) any { return srv.world.Sites[id] })
srv.RegisterResourcePage("/artifact/{id}", "artifact.html", func(id int) any { return srv.world.Artifacts[id] })
srv.RegisterPage("/events", "eventTypes.html", func(p Parms) any { return srv.world.AllEventTypes() })
srv.RegisterPage("/events/{type}", "eventType.html", func(p Parms) any { return srv.world.EventsOfType(p["type"]) })
spa := spaHandler{staticFS: static, staticPath: "static", indexPath: "index.html"}
srv.router.PathPrefix("/").Handler(spa)
OpenBrowser("http://localhost:8080")
fmt.Println("Serving at :8080")
http.ListenAndServe(":8080", srv.router)
}
type spaHandler struct {
staticFS embed.FS
staticPath string
indexPath string
}
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// get the absolute path to prevent directory traversal
path := r.URL.Path
// if err != nil {
// // if we failed to get the absolute path respond with a 400 bad request and stop
// http.Error(w, err.Error(), http.StatusBadRequest)
// return
// }
// prepend the path with the path to the static directory
path = h.staticPath + path
fmt.Println(r.URL, "->", path)
_, err := h.staticFS.Open(path)
if os.IsNotExist(err) {
// file does not exist, serve index.html
fmt.Println(path)
index, err := h.staticFS.ReadFile(h.staticPath + "/" + h.indexPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusAccepted)
w.Write(index)
return
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// get the subdirectory of the static dir
statics, err := fs.Sub(h.staticFS, h.staticPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// otherwise, use http.FileServer to serve the static dir
http.FileServer(http.FS(statics)).ServeHTTP(w, r)
}

View File

@ -0,0 +1,38 @@
package server
import (
"fmt"
"html/template"
"github.com/robertjanetzko/LegendsBrowser2/backend/model"
"github.com/robertjanetzko/LegendsBrowser2/backend/templates"
"github.com/robertjanetzko/LegendsBrowser2/backend/util"
)
func (srv *DfServer) LoadTemplates() {
functions := template.FuncMap{
"json": util.Json,
"check": func(condition bool, v any) any {
if condition {
return v
}
return nil
},
"title": util.Title,
"hf": func(id int) template.HTML { return model.LinkHf(srv.world, id) },
"getHf": func(id int) *model.HistoricalFigure { return srv.world.HistoricalFigures[id] },
"entity": func(id int) template.HTML { return model.LinkEntity(srv.world, id) },
"getEntity": func(id int) *model.Entity { return srv.world.Entities[id] },
"site": func(id int) template.HTML { return model.LinkSite(srv.world, id) },
"getSite": func(id int) *model.Site { return srv.world.Sites[id] },
"region": func(id int) template.HTML { return model.LinkRegion(srv.world, id) },
"getRegion": func(id int) *model.Region { return srv.world.Regions[id] },
"events": func(obj any) *model.EventList { return model.NewEventList(srv.world, obj) },
"season": model.Season,
"time": model.Time,
"html": func(value any) template.HTML {
return template.HTML(fmt.Sprint(value))
},
}
srv.templates = templates.New(functions)
}

27
backend/server/util.go Normal file
View File

@ -0,0 +1,27 @@
package server
import (
"fmt"
"os/exec"
"runtime"
)
func OpenBrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
fmt.Println(err)
fmt.Println("navigate to http://localhost:8080 in your browser")
}
}