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