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 = ` Arma 3

Arma 3 Mods

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.

{{range .Mods}} {{end}}
{{.Name | safeHTML}} Steam https://steamcommunity.com/sharedfiles/filedetails/?id={{.ID}}
` 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 }