hf search

This commit is contained in:
Robert Janetzko 2022-05-05 20:18:31 +00:00
parent e9cde0f17c
commit b63c76f48c
17 changed files with 255 additions and 21 deletions

3
.gitignore vendored
View File

@ -6,4 +6,5 @@ legendsbrowser
/*.json /*.json
.DS_Store .DS_Store
bin bin
/inputs/* /inputs/*
/inputs2/*

View File

@ -53,6 +53,10 @@
} }
], ],
"HistoricalFigure": [ "HistoricalFigure": [
{
"Name": "Leader",
"Type": "bool"
},
{ {
"Name": "Werebeast", "Name": "Werebeast",
"Type": "bool" "Type": "bool"
@ -79,6 +83,10 @@
} }
], ],
"Entity": [ "Entity": [
{
"Name": "Leaders",
"Type": "[]*EntityLeader"
},
{ {
"Name": "Sites", "Name": "Sites",
"Type": "[]int" "Type": "[]int"

4
backend/.gitignore vendored
View File

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

View File

@ -1,5 +0,0 @@
{
"LastPath": "/workspaces/legendsbrowser/inputs/legends-Runlance-01510-01-01",
"LastFile": "/workspaces/legendsbrowser/inputs/legends-Runlance-01510-01-01/Runlance-01510-01-01-legends.xml",
"DebugTemplates": true
}

View File

@ -57,7 +57,7 @@ func (c *Context) hfUnrelated(id int) string {
func (c *Context) hfShort(id int) string { func (c *Context) hfShort(id int) string {
if x, ok := c.World.HistoricalFigures[id]; ok { 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 fmt.Sprintf(`<a class="hf" href="/hf/%d">%s%s</a>`, x.Id(), hfIcon(x), util.Title(x.FirstName()))
} }
return "UNKNOWN HISTORICAL FIGURE" return "UNKNOWN HISTORICAL FIGURE"
} }
@ -73,7 +73,7 @@ func (c *Context) hfRelated(id, to int) string {
if x, ok := c.World.HistoricalFigures[id]; ok { if x, ok := c.World.HistoricalFigures[id]; ok {
if t, ok := c.World.HistoricalFigures[to]; 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 { 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(`%s %s <a class="hf" href="/hf/%d">%s%s</a>`, t.PossesivePronoun(), y.LinkType, x.Id(), hfIcon(x), util.Title(x.Name()))
} }
} }
return hf(x) return hf(x)
@ -81,6 +81,14 @@ func (c *Context) hfRelated(id, to int) string {
return "UNKNOWN HISTORICAL FIGURE" return "UNKNOWN HISTORICAL FIGURE"
} }
func hfIcon(x *HistoricalFigure) string {
switch {
case x.Leader:
return `<i class="fa-solid fa-crown fa-xs"></i> `
}
return ""
}
func hf(x *HistoricalFigure) string { func hf(x *HistoricalFigure) string {
r := x.Race r := x.Race
if x.Deity { if x.Deity {
@ -98,7 +106,7 @@ func hf(x *HistoricalFigure) string {
if x.Vampire { if x.Vampire {
r += " vampire" r += " vampire"
} }
return fmt.Sprintf(`the %s <a class="hf" href="/hf/%d">%s</a>`, r, x.Id(), util.Title(x.Name())) return fmt.Sprintf(`the %s <a class="hf" href="/hf/%d">%s%s</a>`, r, x.Id(), hfIcon(x), util.Title(x.Name()))
} }
func (c *Context) hfList(ids []int) string { func (c *Context) hfList(ids []int) string {
@ -152,7 +160,7 @@ func (c *Context) siteStructure(siteId, structureId int, prefix string) string {
func (c *Context) site(id int, prefix string) string { func (c *Context) site(id int, prefix string) string {
if x, ok := c.World.Sites[id]; ok { if x, ok := c.World.Sites[id]; ok {
return fmt.Sprintf(`%s <a class="site%s" href="/site/%d"><i class="%s fa-xs"></i> %s</a>`, prefix, util.If(x.Ruin, " ruin", ""), x.Id(), x.Icon(), util.Title(x.Name())) return fmt.Sprintf(`%s <a class="site" href="/site/%d"><i class="%s fa-xs%s"></i> %s</a>`, prefix, x.Id(), x.Icon(), util.If(x.Ruin, " ruin", ""), util.Title(x.Name()))
} }
return "UNKNOWN SITE" return "UNKNOWN SITE"
} }
@ -160,7 +168,7 @@ func (c *Context) site(id int, prefix string) string {
func (c *Context) structure(siteId, structureId int) string { func (c *Context) structure(siteId, structureId int) string {
if x, ok := c.World.Sites[siteId]; ok { if x, ok := c.World.Sites[siteId]; ok {
if y, ok := x.Structures[structureId]; ok { if y, ok := x.Structures[structureId]; ok {
return fmt.Sprintf(`<a class="structure%s" href="/site/%d/structure/%d"><i class="%s fa-xs"></i> %s</a>`, util.If(y.Ruin, " ruin", ""), siteId, structureId, y.Icon(), util.Title(y.Name())) return fmt.Sprintf(`<a class="structure" href="/site/%d/structure/%d"><i class="%s fa-xs%s"></i> %s</a>`, siteId, structureId, y.Icon(), util.If(y.Ruin, " ruin", ""), util.Title(y.Name()))
} }
} }
return "UNKNOWN STRUCTURE" return "UNKNOWN STRUCTURE"

View File

@ -9,6 +9,7 @@ import (
) )
var LinkHf = func(w *DfWorld, id int) template.HTML { return template.HTML((&Context{World: w}).hf(id)) } var LinkHf = func(w *DfWorld, id int) template.HTML { return template.HTML((&Context{World: w}).hf(id)) }
var LinkHfShort = func(w *DfWorld, id int) template.HTML { return template.HTML((&Context{World: w}).hfShort(id)) }
var LinkHfList = func(w *DfWorld, id []int) template.HTML { return template.HTML((&Context{World: w}).hfList(id)) } var LinkHfList = func(w *DfWorld, id []int) template.HTML { return template.HTML((&Context{World: w}).hfList(id)) }
var LinkEntity = func(w *DfWorld, id int) template.HTML { return template.HTML((&Context{World: w}).entity(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 LinkSite = func(w *DfWorld, id int) template.HTML { return template.HTML((&Context{World: w}).site(id, "")) }

55
backend/model/history.go Normal file
View File

@ -0,0 +1,55 @@
package model
import (
"fmt"
"io/ioutil"
"regexp"
"strconv"
"strings"
"github.com/robertjanetzko/LegendsBrowser2/backend/util"
)
type EntityLeader struct {
Hf *HistoricalFigure
StartYear int
EndYear int
}
func (w *DfWorld) LoadHistory() {
w.LoadDimensions()
path := strings.ReplaceAll(w.FilePath, "-legends.xml", "-world_history.txt")
data, err := ioutil.ReadFile(path)
if err != nil {
fmt.Println(err)
return
}
leaderRegEx := regexp.MustCompile(` \[\*\] (.+?) \(.*?Reign Began: (-?\d+)\)`)
results := regexp.MustCompile(`\n([^ ].*?), [^\n]+(?:\n [^\n]+)*`).FindAllStringSubmatch(util.ConvertCp473(data), -1)
for _, result := range results {
if _, civ, ok := util.FindInMap(w.Entities, nameMatches[*Entity](result[1])); ok {
leaders := leaderRegEx.FindAllStringSubmatch(result[0], -1)
var last *EntityLeader
for _, leader := range leaders {
year, _ := strconv.Atoi(leader[2])
l := &EntityLeader{StartYear: year, EndYear: -1}
if _, hf, ok := util.FindInMap(w.HistoricalFigures, nameMatches[*HistoricalFigure](leader[1])); ok {
hf.Leader = true
l.Hf = hf
civ.Leaders = append(civ.Leaders, l)
}
if last != nil {
last.EndYear = year
}
last = l
}
}
}
}
func nameMatches[T Named](name string) func(T) bool {
name = strings.ToLower(name)
return func(t T) bool { return t.Name() == name }
}

View File

@ -74,17 +74,13 @@ func (w *DfWorld) LoadDimensions() {
} }
fmt.Println("Found Worldgen", path) fmt.Println("Found Worldgen", path)
content, err := ioutil.ReadFile(path) content, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return return
} }
text := string(content)
fmt.Println(text)
r := regexp.MustCompile(`\[DIM:(\d+):(\d+)\]`) r := regexp.MustCompile(`\[DIM:(\d+):(\d+)\]`)
result := r.FindAllStringSubmatch(text, 1) result := r.FindAllStringSubmatch(string(content), 1)
if result == nil { if result == nil {
return return
} }

View File

@ -1990,6 +1990,7 @@ type Entity struct {
Type_ EntityType `json:"type" legend:"plus" related:""` // type Type_ EntityType `json:"type" legend:"plus" related:""` // type
Weapon []EntityWeapon `json:"weapon" legend:"plus" related:""` // weapon Weapon []EntityWeapon `json:"weapon" legend:"plus" related:""` // weapon
WorshipId []int `json:"worshipId" legend:"plus" related:""` // worship_id WorshipId []int `json:"worshipId" legend:"plus" related:""` // worship_id
Leaders []*EntityLeader `json:"leaders" legend:"add" related:""` // Leaders
Sites []int `json:"sites" legend:"add" related:""` // Sites Sites []int `json:"sites" legend:"add" related:""` // Sites
Wars []*HistoricalEventCollection `json:"wars" legend:"add" related:""` // Wars Wars []*HistoricalEventCollection `json:"wars" legend:"add" related:""` // Wars
} }
@ -2029,6 +2030,7 @@ func (x *Entity) MarshalJSON() ([]byte, error) {
} }
d["weapon"] = x.Weapon d["weapon"] = x.Weapon
d["worshipId"] = x.WorshipId d["worshipId"] = x.WorshipId
d["leaders"] = x.Leaders
d["sites"] = x.Sites d["sites"] = x.Sites
d["wars"] = x.Wars d["wars"] = x.Wars
return json.Marshal(d) return json.Marshal(d)
@ -18900,6 +18902,7 @@ type HistoricalFigure struct {
Sphere []string `json:"sphere" legend:"base" related:""` // sphere Sphere []string `json:"sphere" legend:"base" related:""` // sphere
UsedIdentityId []int `json:"usedIdentityId" legend:"base" related:""` // used_identity_id UsedIdentityId []int `json:"usedIdentityId" legend:"base" related:""` // used_identity_id
VagueRelationship []*VagueRelationship `json:"vagueRelationship" legend:"base" related:""` // vague_relationship VagueRelationship []*VagueRelationship `json:"vagueRelationship" legend:"base" related:""` // vague_relationship
Leader bool `json:"leader" legend:"add" related:""` // Leader
Necromancer bool `json:"necromancer" legend:"add" related:""` // Necromancer Necromancer bool `json:"necromancer" legend:"add" related:""` // Necromancer
NecromancerSince int `json:"necromancerSince" legend:"add" related:""` // NecromancerSince NecromancerSince int `json:"necromancerSince" legend:"add" related:""` // NecromancerSince
Vampire bool `json:"vampire" legend:"add" related:""` // Vampire Vampire bool `json:"vampire" legend:"add" related:""` // Vampire
@ -19022,6 +19025,7 @@ func (x *HistoricalFigure) MarshalJSON() ([]byte, error) {
d["sphere"] = x.Sphere d["sphere"] = x.Sphere
d["usedIdentityId"] = x.UsedIdentityId d["usedIdentityId"] = x.UsedIdentityId
d["vagueRelationship"] = x.VagueRelationship d["vagueRelationship"] = x.VagueRelationship
d["leader"] = x.Leader
d["necromancer"] = x.Necromancer d["necromancer"] = x.Necromancer
if x.NecromancerSince != -1 { if x.NecromancerSince != -1 {
d["necromancerSince"] = x.NecromancerSince d["necromancerSince"] = x.NecromancerSince

View File

@ -143,6 +143,7 @@ BaseLoop:
// ioutil.WriteFile("same.json", same, 0644) // ioutil.WriteFile("same.json", same, 0644)
world.LoadMap() world.LoadMap()
world.LoadHistory()
world.process() world.process()

View File

@ -18,7 +18,12 @@ func (srv *DfServer) RegisterWorldPage(path string, template string, accessor fu
return return
} }
data := accessor(mux.Vars(r)) params := mux.Vars(r)
for k, v := range r.URL.Query() {
params[k] = v[0]
}
data := accessor(params)
if data == nil || (reflect.ValueOf(data).Kind() == reflect.Ptr && reflect.ValueOf(data).IsNil()) { if data == nil || (reflect.ValueOf(data).Kind() == reflect.Ptr && reflect.ValueOf(data).IsNil()) {
srv.notFound(w) srv.notFound(w)
return return

View File

@ -106,6 +106,7 @@ func StartServer(config *Config, world *model.DfWorld, static embed.FS) error {
srv.RegisterWorldResourcePage("/writtencontent/{id}", "writtencontent.html", func(id int) any { return srv.context.world.WrittenContents[id] }) srv.RegisterWorldResourcePage("/writtencontent/{id}", "writtencontent.html", func(id int) any { return srv.context.world.WrittenContents[id] })
srv.RegisterWorldResourcePage("/popover/writtencontent/{id}", "popoverWrittencontent.html", func(id int) any { return srv.context.world.WrittenContents[id] }) srv.RegisterWorldResourcePage("/popover/writtencontent/{id}", "popoverWrittencontent.html", func(id int) any { return srv.context.world.WrittenContents[id] })
srv.RegisterWorldPage("/hfs", "hfs.html", srv.searchHf)
srv.RegisterWorldResourcePage("/hf/{id}", "hf.html", func(id int) any { return srv.context.world.HistoricalFigures[id] }) srv.RegisterWorldResourcePage("/hf/{id}", "hf.html", func(id int) any { return srv.context.world.HistoricalFigures[id] })
srv.RegisterWorldResourcePage("/popover/hf/{id}", "popoverHf.html", func(id int) any { return srv.context.world.HistoricalFigures[id] }) srv.RegisterWorldResourcePage("/popover/hf/{id}", "popoverHf.html", func(id int) any { return srv.context.world.HistoricalFigures[id] })
@ -172,6 +173,48 @@ func (srv *DfServer) findStructure(p Parms) any {
return nil return nil
} }
func (srv *DfServer) searchHf(p Parms) any {
var list []*model.HistoricalFigure
for _, hf := range srv.context.world.HistoricalFigures {
if p["leader"] == "1" && !hf.Leader {
continue
}
if p["deity"] == "1" && !hf.Deity {
continue
}
if p["force"] == "1" && !hf.Force {
continue
}
if p["vampire"] == "1" && !hf.Vampire {
continue
}
if p["werebeast"] == "1" && !hf.Werebeast {
continue
}
if p["necromancer"] == "1" && !hf.Necromancer {
continue
}
if p["alive"] == "1" && hf.DeathYear != -1 {
continue
}
if p["ghost"] == "1" && false { // TODO ghost
continue
}
if p["adventurer"] == "1" && !hf.Adventurer {
continue
}
list = append(list, hf)
}
sort.Slice(list, func(i, j int) bool { return list[i].Name_ < list[j].Name_ })
return map[string]any{
"Params": p,
"Hfs": list,
}
}
func (srv *DfServer) notFound(w http.ResponseWriter) { func (srv *DfServer) notFound(w http.ResponseWriter) {
err := srv.templates.Render(w, "notFound.html", nil) err := srv.templates.Render(w, "notFound.html", nil)
if err != nil { if err != nil {

View File

@ -30,6 +30,7 @@ func (srv *DfServer) LoadTemplates() {
srv.context.world.Width, srv.context.world.Height)) srv.context.world.Width, srv.context.world.Height))
}, },
"hf": func(id int) template.HTML { return model.LinkHf(srv.context.world, id) }, "hf": func(id int) template.HTML { return model.LinkHf(srv.context.world, id) },
"hfShort": func(id int) template.HTML { return model.LinkHfShort(srv.context.world, id) },
"getHf": func(id int) *model.HistoricalFigure { return srv.context.world.HistoricalFigures[id] }, "getHf": func(id int) *model.HistoricalFigure { return srv.context.world.HistoricalFigures[id] },
"hfList": func(ids []int) template.HTML { return model.LinkHfList(srv.context.world, ids) }, "hfList": func(ids []int) template.HTML { return model.LinkHfList(srv.context.world, ids) },
"entity": func(id int) template.HTML { return model.LinkEntity(srv.context.world, id) }, "entity": func(id int) template.HTML { return model.LinkEntity(srv.context.world, id) },

View File

@ -13,8 +13,11 @@
<nav> <nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist"> <div class="nav nav-tabs" id="nav-tab" role="tablist">
{{- if gt (len .Leaders) 0 }}
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#nav-leaders" type="button" role="tab">Sites</button>
{{- end}}
{{- if gt (len .Sites) 0 }} {{- if gt (len .Sites) 0 }}
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#nav-sites" type="button" role="tab">Sites</button> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-sites" type="button" role="tab">Sites</button>
{{- end}} {{- end}}
{{- if gt (len .HistfigId) 0 }} {{- if gt (len .HistfigId) 0 }}
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-members" type="button" role="tab">Members</button> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-members" type="button" role="tab">Members</button>
@ -28,8 +31,31 @@
</div> </div>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
{{- if gt (len .Leaders) 0 }}
<div class="tab-pane active" id="nav-leaders" role="tabpanel">
<table class="table table-hover table-sm table-borderless object-table">
<tr>
<th>Date</th>
<th width="100%">Name</th>
</tr>
{{- range .Leaders }}
<tr>
<td>
{{- if eq .EndYear -1 }}
since {{ .StartYear }}
{{- else }}
from {{ .StartYear }} till {{ .EndYear }}
{{- end }}
</td>
<td>
{{ hf .Hf.Id }}</td>
</tr>
{{- end}}
</table>
</div>
{{- end}}
{{- if gt (len .Sites) 0 }} {{- if gt (len .Sites) 0 }}
<div class="tab-pane active" id="nav-sites" role="tabpanel"> <div class="tab-pane" id="nav-sites" role="tabpanel">
<table class="table table-hover table-sm table-borderless"> <table class="table table-hover table-sm table-borderless">
<tr> <tr>
<th>Name</th> <th>Name</th>

View File

@ -10,10 +10,14 @@
<i class="fa-solid fa-mars fa-xs"></i> <i class="fa-solid fa-mars fa-xs"></i>
{{end}} {{end}}
{{ .Race }} {{ .Race }}
{{ if .Deity}}deity{{end}}
{{ if .Force}}force{{end}}
{{ if .Vampire}}vampire{{end}} {{ if .Vampire}}vampire{{end}}
{{ if .Werebeast}}werebeast{{end}} {{ if .Werebeast}}werebeast{{end}}
{{ if .Necromancer}}necromancer{{end}} {{ if .Necromancer}}necromancer{{end}}
{{ if not (or .Deity .Force)}}
(*{{ .BirthYear }}{{ if ge .DeathYear 0 }} †{{ .DeathYear }}{{ end }}) (*{{ .BirthYear }}{{ if ge .DeathYear 0 }} †{{ .DeathYear }}{{ end }})
{{ end }}
<div class="row mt-3"> <div class="row mt-3">
<div class="col-4"> <div class="col-4">

View File

@ -0,0 +1,76 @@
{{template "layout.html" .}}
{{define "title"}}Historical Figures{{end}}
{{define "content"}}
<h3>Historical Figures</h3>
{{ json .Params }}
<div class="row">
<div class="col-md-10">
<table class="table table-hover table-sm table-borderless object-table">
<tr>
<th>Name</th>
<th>Race</th>
<th>Lived</th>
</tr>
{{- range .Hfs }}{{- if not (eq .Name "") }}
<tr>
<td><a class="hf" href="/hf/{{.Id}}">{{ title .Name }}</a></td>
<td>{{ .Race }}</td>
<td>
{{- if eq .DeathYear -1 }}
from {{ .BirthYear }} till now
{{- else }}
from {{ .BirthYear }} till {{ .DeathYear }}
{{- end }}
</td>
</tr>
{{- end}}{{- end}}
</table>
</div>
<div class="col-md-2">
<h5>Filter</h5>
<form action="/hfs" method="GET">
<div class="checkbox"><label><input class="filter" type="checkbox" name="leader" value="1" {{if eq .Params.leader "1"
}}checked{{end}}> Leader</label></div>
<div class="checkbox"><label><input class="filter" type="checkbox" name="deity" value="1" {{if eq .Params.deity "1"
}}checked{{end}}> Deity</label></div>
<div class="checkbox"><label><input class="filter" type="checkbox" name="force" value="1" {{if eq .Params.force "1"
}}checked{{end}}> Force</label></div>
<div class="checkbox"><label><input class="filter" type="checkbox" name="vampire" value="1" {{if eq .Params.vampire "1"
}}checked{{end}}> Vampire</label></div>
<div class="checkbox"><label><input class="filter" type="checkbox" name="werebeast" value="1" {{if eq .Params.werebeast "1"
}}checked{{end}}> Werebeast</label></div>
<div class="checkbox"><label><input class="filter" type="checkbox" name="necromancer" value="1" {{if eq .Params.necromancer "1"
}}checked{{end}}> Necromancer</label></div>
<div class="checkbox"><label><input class="filter" type="checkbox" name="alive" value="1" {{if eq .Params.alive "1"
}}checked{{end}}> Alive</label></div>
<div class="checkbox"><label><input class="filter" type="checkbox" name="ghost" value="1" {{if eq .Params.ghost "1"
}}checked{{end}}> Ghost</label></div>
<div class="checkbox"><label><input class="filter" type="checkbox" name="adventurer" value="1" {{if eq .Params.adventurer "1"
}}checked{{end}}> Adventurer</label></div>
<div class="select form-group">
<select class="form-control" name="race">
<option class="text-muted" value="">Race</option>
<option value="black bear">black bear</option>
</select>
</div>
<h4>Sorting</h4>
<div class="select form-group">
<select class="form-control" name="sort">
<option value="">Default</option>
<option value="name">Name</option>
<option value="race">Race</option>
<option value="birth">Birth</option>
<option value="death">Death</option>
<option value="kills">Kills</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Refresh</button>
</form>
</div>
</div>
{{- end }}

View File

@ -3,7 +3,15 @@
{{else}} {{else}}
<i class="fa-solid fa-mars fa-xs"></i> <i class="fa-solid fa-mars fa-xs"></i>
{{end}} {{end}}
{{ .Race }} (*{{ .BirthYear }}{{ if ge .DeathYear 0 }} †{{ .DeathYear }}{{ end }}) {{ .Race }}
{{ if .Deity}}deity{{end}}
{{ if .Force}}force{{end}}
{{ if .Vampire}}vampire{{end}}
{{ if .Werebeast}}werebeast{{end}}
{{ if .Necromancer}}necromancer{{end}}
{{ if not (or .Deity .Force)}}
(*{{ .BirthYear }}{{ if ge .DeathYear 0 }} †{{ .DeathYear }}{{ end }})
{{ end }}
{{- if or (ne 0 (len .EntityFormerPositionLink)) (ne 0 (len .EntityPositionLink)) }} {{- if or (ne 0 (len .EntityFormerPositionLink)) (ne 0 (len .EntityPositionLink)) }}
<ul class="mb-0"> <ul class="mb-0">