Files
ArmA-3-web-server/backend/internal/services/modlist_parser.go
MrFastwind 6774397885 feat: add modlist export to Arma 3 Launcher HTML format
- RenderModlistHTML: generates compatible preset HTML from a modlist
- GET /api/modlists/:id/export: returns downloadable .html file
- Export button on ModlistEditor page
- Skips local mods (no workshop ID), includes only Steam workshop mods
2026-07-06 18:33:24 +02:00

184 lines
4.3 KiB
Go

package services
import (
"bytes"
"fmt"
"html/template"
"io"
"net/url"
"path"
"strings"
"arma3-web-server/internal/models"
"golang.org/x/net/html"
)
const modlistHTMLTmpl = `<?xml version="1.0" encoding="utf-8"?>
<html>
<head>
<meta name="arma:Type" content="list"/>
<meta name="generator" content="Arma 3 Web Server"/>
<title>Arma 3</title>
<style>
body{margin:0;padding:0;color:#fff;background:#000;font:95%/1.3 Roboto,Segoe UI,Tahoma,Arial,Helvetica,sans-serif}
td{padding:3px 30px 3px 0}
h1{padding:20px 20px 0;color:#fff;font-weight:200;font-family:segoe ui;font-size:3em;margin:0}
.before-list{padding:5px 20px 10px}
.mod-list{background:#222;padding:20px}
.footer{padding:20px;color:gray}
.from-steam{color:#449EBD}
</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}}</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 RenderModlistHTML(name string, mods []models.ModEntry) (string, error) {
tmpl, err := template.New("modlist").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
}