Eric Bower
·
2026-01-16
listparser.go
1package shared
2
3import (
4 "fmt"
5 "html/template"
6 "log"
7 "regexp"
8 "strconv"
9 "strings"
10 "time"
11
12 "github.com/araddon/dateparse"
13)
14
15/*
16=> https://www.youtube.com/watch?v=HxaD_trXwRE Lexical Scanning in Go - Rob Pike
17func run() {
18 for state := startState; state != nil {
19 state = stateFn(lexer);
20 }
21}
22*/
23
24var reIndent = regexp.MustCompile(`^[[:blank:]]+`)
25
26type ListParsedText struct {
27 Items []*ListItem
28 *ListMetaData
29}
30
31type ListItem struct {
32 Value string
33 URL template.URL
34 Variable string
35 IsURL bool
36 IsBlock bool
37 IsText bool
38 IsHeaderOne bool
39 IsHeaderTwo bool
40 IsImg bool
41 IsPre bool
42 IsHr bool
43 Indent int
44}
45
46type ListMetaData struct {
47 // prose
48 Aliases []string
49 Description string
50 Hidden bool
51 Image string
52 ImageCard string
53 Layout string
54 PublishAt *time.Time
55 Tags []string
56 Title string
57
58 // feeds
59 DigestInterval string
60 Cron string
61 Email string
62 InlineContent bool // allows content inlining to be disabled in feeds.pico.sh emails
63}
64
65var urlToken = "=>"
66var blockToken = ">"
67var varToken = "=:"
68var imgToken = "=<"
69var headerOneToken = "#"
70var headerTwoToken = "##"
71var preToken = "```"
72var hrToken = "=="
73
74type SplitToken struct {
75 Key string
76 Value string
77}
78
79func TextToSplitToken(text string) *SplitToken {
80 txt := strings.Trim(text, " ")
81 token := &SplitToken{}
82 word := ""
83 for i, c := range txt {
84 if c == ' ' {
85 token.Key = strings.Trim(word, " ")
86 token.Value = strings.Trim(txt[i:], " ")
87 break
88 } else {
89 word += string(c)
90 }
91 }
92
93 if token.Key == "" {
94 token.Key = strings.Trim(text, " ")
95 token.Value = strings.Trim(text, " ")
96 }
97
98 return token
99}
100
101func SplitByNewline(text string) []string {
102 return strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
103}
104
105func PublishAtDate(date string) (*time.Time, error) {
106 t, err := dateparse.ParseStrict(date)
107 return &t, err
108}
109
110func TokenToMetaField(meta *ListMetaData, token *SplitToken) error {
111 switch token.Key {
112 case "date":
113 publishAt, err := PublishAtDate(token.Value)
114 if err == nil {
115 meta.PublishAt = publishAt
116 }
117 case "title":
118 meta.Title = token.Value
119 case "description":
120 meta.Description = token.Value
121 case "image":
122 meta.Image = token.Value
123 case "image_card":
124 meta.ImageCard = token.Value
125 case "draft":
126 if token.Value == "true" {
127 meta.Hidden = true
128 } else {
129 meta.Hidden = false
130 }
131 case "tags":
132 tags := strings.Split(token.Value, ",")
133 meta.Tags = make([]string, 0)
134 for _, tag := range tags {
135 meta.Tags = append(meta.Tags, strings.TrimSpace(tag))
136 }
137 case "aliases":
138 aliases := strings.Split(token.Value, ",")
139 meta.Aliases = make([]string, 0)
140 for _, alias := range aliases {
141 meta.Aliases = append(meta.Aliases, strings.TrimSpace(alias))
142 }
143 case "layout":
144 meta.Layout = token.Value
145 case "digest_interval":
146 meta.DigestInterval = token.Value
147 case "cron":
148 meta.Cron = token.Value
149 case "email":
150 meta.Email = token.Value
151 case "inline_content":
152 v, err := strconv.ParseBool(token.Value)
153 if err != nil {
154 // its empty or its improperly configured, just send the content
155 v = true
156 }
157 meta.InlineContent = v
158 }
159
160 return nil
161}
162
163func KeyAsValue(token *SplitToken) string {
164 if token.Value == "" {
165 return token.Key
166 }
167 return token.Value
168}
169
170func parseItem(meta *ListMetaData, li *ListItem, prevItem *ListItem, pre bool, mod int) (bool, bool, int) {
171 skip := false
172
173 if strings.HasPrefix(li.Value, preToken) {
174 pre = !pre
175 if pre {
176 nextValue := strings.Replace(li.Value, preToken, "", 1)
177 li.IsPre = true
178 li.Value = nextValue
179 } else {
180 skip = true
181 }
182 } else if pre {
183 nextValue := strings.Replace(li.Value, preToken, "", 1)
184 prevItem.Value = fmt.Sprintf("%s\n%s", prevItem.Value, nextValue)
185 skip = true
186 } else if strings.HasPrefix(li.Value, urlToken) {
187 li.IsURL = true
188 split := TextToSplitToken(strings.Replace(li.Value, urlToken, "", 1))
189 li.URL = template.URL(split.Key)
190 li.Value = KeyAsValue(split)
191 } else if strings.HasPrefix(li.Value, blockToken) {
192 li.IsBlock = true
193 li.Value = strings.Replace(li.Value, blockToken, "", 1)
194 } else if strings.HasPrefix(li.Value, imgToken) {
195 li.IsImg = true
196 split := TextToSplitToken(strings.Replace(li.Value, imgToken, "", 1))
197 key := split.Key
198 li.URL = template.URL(key)
199 li.Value = KeyAsValue(split)
200 } else if strings.HasPrefix(li.Value, varToken) {
201 split := TextToSplitToken(strings.Replace(li.Value, varToken, "", 1))
202 err := TokenToMetaField(meta, split)
203 if err != nil {
204 log.Println(err)
205 }
206 } else if strings.HasPrefix(li.Value, headerTwoToken) {
207 li.IsHeaderTwo = true
208 li.Value = strings.Replace(li.Value, headerTwoToken, "", 1)
209 } else if strings.HasPrefix(li.Value, headerOneToken) {
210 li.IsHeaderOne = true
211 li.Value = strings.Replace(li.Value, headerOneToken, "", 1)
212 } else if strings.HasPrefix(li.Value, hrToken) {
213 li.IsHr = true
214 li.Value = ""
215 } else if reIndent.MatchString(li.Value) {
216 trim := reIndent.ReplaceAllString(li.Value, "")
217 old := len(li.Value)
218 li.Value = trim
219
220 pre, skip, _ = parseItem(meta, li, prevItem, pre, mod)
221 if prevItem != nil && prevItem.Indent == 0 {
222 mod = old - len(trim)
223 li.Indent = 1
224 } else {
225 numerator := old - len(trim)
226 if mod == 0 {
227 li.Indent = 1
228 } else {
229 li.Indent = numerator / mod
230 }
231 }
232 } else {
233 li.IsText = true
234 }
235
236 return pre, skip, mod
237}
238
239func ListParseText(text string) *ListParsedText {
240 textItems := SplitByNewline(text)
241 items := []*ListItem{}
242 meta := ListMetaData{
243 Aliases: []string{},
244 InlineContent: true,
245 Layout: "default",
246 PublishAt: &time.Time{},
247 Tags: []string{},
248 Hidden: false,
249 }
250 pre := false
251 skip := false
252 mod := 0
253 var prevItem *ListItem
254
255 for _, t := range textItems {
256 if len(items) > 0 {
257 prevItem = items[len(items)-1]
258 }
259
260 li := ListItem{
261 Value: t,
262 }
263
264 pre, skip, mod = parseItem(&meta, &li, prevItem, pre, mod)
265
266 if li.IsText && li.Value == "" {
267 skip = true
268 }
269
270 if !skip {
271 items = append(items, &li)
272 }
273 }
274
275 return &ListParsedText{
276 Items: items,
277 ListMetaData: &meta,
278 }
279}