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