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}