main.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: BSD-2-Clause
  4
  5package main
  6
  7import (
  8	"bytes"
  9	"encoding/xml"
 10	"fmt"
 11	"os"
 12	"strings"
 13	"text/template"
 14
 15	flag "github.com/spf13/pflag"
 16)
 17
 18var (
 19	flagInput    *string = flag.StringP("input", "i", "", "Input file")
 20	flagOutput   *string = flag.StringP("output", "o", "", "Output file")
 21	flagTemplate *string = flag.StringP("template", "t", "", "Template file")
 22	flagIgnore   *string = flag.StringP("ignore", "g", "", "Comma-separated list of sections to ignore")
 23	flagHelp     *bool   = flag.BoolP("help", "h", false, "Show help and exit")
 24)
 25
 26type OPML struct {
 27	XMLName xml.Name `xml:"opml"`
 28	Version string   `xml:"version,attr"`
 29	Head    Head     `xml:"head"`
 30	Body    Body     `xml:"body"`
 31}
 32
 33type Head struct {
 34	Title           string `xml:"title"`
 35	DateCreated     string `xml:"dateCreated,omitempty"`
 36	DateModified    string `xml:"dateModified,omitempty"`
 37	OwnerName       string `xml:"ownerName,omitempty"`
 38	OwnerEmail      string `xml:"ownerEmail,omitempty"`
 39	OwnerID         string `xml:"ownerId,omitempty"`
 40	Docs            string `xml:"docs,omitempty"`
 41	ExpansionState  string `xml:"expansionState,omitempty"`
 42	VertScrollState string `xml:"vertScrollState,omitempty"`
 43	WindowTop       string `xml:"windowTop,omitempty"`
 44	WindowBottom    string `xml:"windowBottom,omitempty"`
 45	WindowLeft      string `xml:"windowLeft,omitempty"`
 46	WindowRight     string `xml:"windowRight,omitempty"`
 47}
 48
 49type Body struct {
 50	Outlines []Outline `xml:"outline"`
 51}
 52
 53type Outline struct {
 54	Outlines     []Outline `xml:"outline"`
 55	Text         string    `xml:"text,attr"`
 56	Type         string    `xml:"type,attr,omitempty"`
 57	IsComment    string    `xml:"isComment,attr,omitempty"`
 58	IsBreakpoint string    `xml:"isBreakpoint,attr,omitempty"`
 59	Created      string    `xml:"created,attr,omitempty"`
 60	Category     string    `xml:"category,attr,omitempty"`
 61	XMLURL       string    `xml:"xmlUrl,attr,omitempty"`
 62	HTMLURL      string    `xml:"htmlUrl,attr,omitempty"`
 63	URL          string    `xml:"url,attr,omitempty"`
 64	Language     string    `xml:"language,attr,omitempty"`
 65	Title        string    `xml:"title,attr,omitempty"`
 66	Version      string    `xml:"version,attr,omitempty"`
 67	Description  string    `xml:"description,attr,omitempty"`
 68}
 69
 70func main() {
 71	flags()
 72	opml, err := parseOPML(*flagInput)
 73	if err != nil {
 74		fmt.Println(err)
 75		os.Exit(1)
 76	}
 77
 78	feedsList := opmlToList(opml.Body.Outlines)
 79
 80	str, err := feedsToFile(feedsList)
 81	if err != nil {
 82		fmt.Println(err)
 83		os.Exit(1)
 84	}
 85
 86	err = os.WriteFile(*flagOutput, []byte(str), 0o644)
 87	if err != nil {
 88		fmt.Println(err)
 89		os.Exit(1)
 90	}
 91
 92	fmt.Printf("Wrote %s\n", *flagOutput)
 93}
 94
 95func parseOPML(filename string) (*OPML, error) {
 96	// Read filename into []byte
 97	file, err := os.ReadFile(filename)
 98	if err != nil {
 99		fmt.Println(err)
100		os.Exit(1)
101	}
102
103	var feeds OPML
104	err = xml.Unmarshal(file, &feeds)
105	if err != nil {
106		return nil, err
107	}
108
109	return &feeds, nil
110}
111
112func opmlToList(outlines []Outline) string {
113	var feedsList string
114	ignoreMap := make(map[string]bool)
115	if *flagIgnore != "" {
116		ignore := strings.Split(*flagIgnore, ",")
117		for _, i := range ignore {
118			ignoreMap[i] = true
119		}
120	}
121	for _, outline := range outlines {
122		if outline.Outlines != nil {
123			if ignoreMap[outline.Text] {
124				continue
125			}
126			feedsList += fmt.Sprintf("\n### %s\n\n", outline.Text)
127			feedsList += opmlToList(outline.Outlines)
128		} else {
129			feedsList += fmt.Sprintf("- [%s](%s) [(Feed)](%s)\n", outline.Text, outline.HTMLURL, outline.XMLURL)
130		}
131	}
132	return feedsList
133}
134
135func feedsToFile(feedsList string) (string, error) {
136	// Read template into []byte
137	templateFile, err := os.ReadFile(*flagTemplate)
138	if err != nil {
139		return "", err
140	}
141
142	tmpl, err := template.New("feeds").Parse(string(templateFile))
143	if err != nil {
144		return "", err
145	}
146
147	var buf bytes.Buffer
148	err = tmpl.Execute(&buf, feedsList)
149	if err != nil {
150		return "", err
151	}
152
153	return buf.String(), nil
154}
155
156func flags() {
157	flag.Parse()
158	if *flagHelp {
159		fmt.Println("Usage: opml2md -i input.opml -o output.md -t template.md")
160		flag.PrintDefaults()
161		fmt.Println("\nTemplate should contain {{ . }} to be replaced with the")
162		fmt.Println("sections, titles, and URLs from the OPML file.")
163		os.Exit(0)
164	}
165	if *flagInput == "" {
166		fmt.Println("Input file is required")
167		return
168	}
169	if _, err := os.Stat(*flagInput); os.IsNotExist(err) {
170		fmt.Println("Input file does not exist")
171		os.Exit(1)
172	}
173	if *flagTemplate == "" {
174		fmt.Println("Template file is required")
175		os.Exit(1)
176	}
177	if _, err := os.Stat(*flagTemplate); os.IsNotExist(err) {
178		fmt.Println("Template file does not exist")
179		os.Exit(1)
180	}
181	if *flagOutput == "" {
182		fmt.Println("Output file is required")
183		os.Exit(1)
184	}
185}