258 lines
5.0 KiB
Go
258 lines
5.0 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
stdhtml "html"
|
|
"io"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"arma3-web-server/internal/models"
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
const modlistHTMLTmpl = `<?xml version="1.0" encoding="utf-8"?>
|
|
<html>
|
|
<!--Created by Arma 3 Web Server-->
|
|
<head>
|
|
<meta name="arma:Type" content="list" />
|
|
<meta name="generator" content="Arma 3 Web Server" />
|
|
<title>Arma 3</title>
|
|
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet" type="text/css" />
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
color: #fff;
|
|
background: #000;
|
|
}
|
|
|
|
body, th, td {
|
|
font: 95%/1.3 Roboto, Segoe UI, Tahoma, Arial, Helvetica, sans-serif;
|
|
}
|
|
|
|
td {
|
|
padding: 3px 30px 3px 0;
|
|
}
|
|
|
|
h1 {
|
|
padding: 20px 20px 0 20px;
|
|
color: white;
|
|
font-weight: 200;
|
|
font-family: segoe ui;
|
|
font-size: 3em;
|
|
margin: 0;
|
|
}
|
|
|
|
em {
|
|
font-variant: italic;
|
|
color:silver;
|
|
}
|
|
|
|
.before-list {
|
|
padding: 5px 20px 10px 20px;
|
|
}
|
|
|
|
.mod-list {
|
|
background: #222222;
|
|
padding: 20px;
|
|
}
|
|
|
|
.dlc-list {
|
|
background: #222222;
|
|
padding: 20px;
|
|
}
|
|
|
|
.footer {
|
|
padding: 20px;
|
|
color:gray;
|
|
}
|
|
|
|
.whups {
|
|
color:gray;
|
|
}
|
|
|
|
a {
|
|
color: #D18F21;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
a:hover {
|
|
color:#F1AF41;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.from-steam {
|
|
color: #449EBD;
|
|
}
|
|
.from-local {
|
|
color: gray;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Arma 3 Mods</h1>
|
|
<p class="before-list">
|
|
<em>To import this preset, drag this file onto the Launcher window. Or click the MODS tab, then PRESET in the top right, then IMPORT at the bottom, and finally select this file.</em>
|
|
</p>
|
|
<div class="mod-list">
|
|
<table>{{range .Mods}}
|
|
<tr data-type="ModContainer">
|
|
<td data-type="DisplayName">{{.Name | safeHTML}}</td>
|
|
<td>
|
|
<span class="from-steam">Steam</span>
|
|
</td>
|
|
<td>
|
|
<a href="https://steamcommunity.com/sharedfiles/filedetails/?id={{.ID}}" data-type="Link">https://steamcommunity.com/sharedfiles/filedetails/?id={{.ID}}</a>
|
|
</td>
|
|
</tr>{{end}}
|
|
</table>
|
|
</div>
|
|
<div class="footer">
|
|
<span>Created by Arma 3 Web Server.</span>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
func ParseModlistHTML(r io.Reader) (string, []models.ModEntry, error) {
|
|
doc, err := html.Parse(r)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("parse html: %w", err)
|
|
}
|
|
|
|
var mods []models.ModEntry
|
|
seen := map[string]bool{}
|
|
|
|
var walk func(*html.Node)
|
|
walk = func(n *html.Node) {
|
|
if n.Type == html.ElementNode && n.Data == "tr" {
|
|
if name, id, ok := extractModRow(n); ok && id != "" {
|
|
if !seen[id] {
|
|
seen[id] = true
|
|
mods = append(mods, models.ModEntry{
|
|
ID: id,
|
|
Name: cleanModName(name),
|
|
Enabled: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
walk(c)
|
|
}
|
|
}
|
|
walk(doc)
|
|
|
|
if mods == nil {
|
|
mods = []models.ModEntry{}
|
|
}
|
|
return "", mods, nil
|
|
}
|
|
|
|
func extractModRow(tr *html.Node) (name, id string, ok bool) {
|
|
var cells []*html.Node
|
|
for c := tr.FirstChild; c != nil; c = c.NextSibling {
|
|
if c.Type == html.ElementNode && c.Data == "td" {
|
|
cells = append(cells, c)
|
|
}
|
|
}
|
|
if len(cells) < 3 {
|
|
return "", "", false
|
|
}
|
|
|
|
name = extractText(cells[0])
|
|
|
|
for c := cells[2].FirstChild; c != nil; c = c.NextSibling {
|
|
if c.Type == html.ElementNode && c.Data == "a" {
|
|
id = extractWorkshopID(c)
|
|
break
|
|
}
|
|
}
|
|
|
|
if name == "" {
|
|
return "", "", false
|
|
}
|
|
return name, id, true
|
|
}
|
|
|
|
func extractText(n *html.Node) string {
|
|
var b strings.Builder
|
|
var walk func(*html.Node)
|
|
walk = func(n *html.Node) {
|
|
if n.Type == html.TextNode {
|
|
b.WriteString(n.Data)
|
|
}
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
walk(c)
|
|
}
|
|
}
|
|
walk(n)
|
|
return strings.TrimSpace(b.String())
|
|
}
|
|
|
|
func extractWorkshopID(a *html.Node) string {
|
|
for _, attr := range a.Attr {
|
|
if attr.Key == "href" {
|
|
u, err := url.Parse(attr.Val)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
return u.Query().Get("id")
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func cleanModName(name string) string {
|
|
name = strings.TrimSpace(name)
|
|
name = strings.ReplaceAll(name, "/", "_")
|
|
name = strings.ReplaceAll(name, "\\", "_")
|
|
return name
|
|
}
|
|
|
|
func FileNameToModlistName(filename string) string {
|
|
ext := path.Ext(filename)
|
|
name := strings.TrimSuffix(filename, ext)
|
|
name = strings.ReplaceAll(name, "_", " ")
|
|
name = strings.ReplaceAll(name, "-", " ")
|
|
return name
|
|
}
|
|
|
|
type renderMod struct {
|
|
Name string
|
|
ID string
|
|
}
|
|
|
|
func safeHTML(s string) string {
|
|
return stdhtml.EscapeString(s)
|
|
}
|
|
|
|
func RenderModlistHTML(name string, mods []models.ModEntry) (string, error) {
|
|
tmpl, err := template.New("modlist").Funcs(template.FuncMap{
|
|
"safeHTML": safeHTML,
|
|
}).Parse(modlistHTMLTmpl)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parse template: %w", err)
|
|
}
|
|
|
|
var renderMods []renderMod
|
|
for _, m := range mods {
|
|
if m.ID == "" {
|
|
continue
|
|
}
|
|
renderMods = append(renderMods, renderMod{
|
|
Name: m.Name,
|
|
ID: m.ID,
|
|
})
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, struct{ Mods []renderMod }{renderMods}); err != nil {
|
|
return "", fmt.Errorf("execute template: %w", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|