[READ-ONLY] Mirror of https://github.com/andrioid/md-social. markdown social media poster (experimental)
1package main
2
3import (
4 "fmt"
5 "io"
6 "log"
7 "net/url"
8 "os"
9 "path/filepath"
10 "regexp"
11 "strings"
12 "time"
13
14 "gopkg.in/yaml.v3"
15)
16
17type MDFile struct {
18 // Frontmatter
19 FM map[string]any
20 // Markdown body
21 Body string
22 // Frontmatter raw
23 RawFM string // without --- markers
24 PendingWrite bool // If we need to write it or not
25 // Relative filename, used for URL if no slug is specified
26 Filename string
27 BaseDir string
28 BaseURL string
29}
30
31var fmRe = regexp.MustCompile(`(?s)^(\s*?)---\s*\n(.*?)\n---\s*\n?`)
32
33func ParseMarkdownFile(file, prefix, baseURL string) (*MDFile, error) {
34 inputData, err := os.ReadFile(file)
35 if err != nil {
36 return nil, err
37 }
38 // Drop BOM
39 input := string(inputData)
40 body := strings.TrimPrefix(input, "\uFEFF")
41 m := fmRe.FindStringSubmatchIndex(input)
42
43 if relativePath, hasPrefix := strings.CutPrefix(file, prefix); hasPrefix {
44 file = relativePath
45 }
46
47 // No frontmatter, no point
48 if m == nil || m[0] != 0 {
49 return nil, fmt.Errorf("%w: no frontmatter", ErrFileSkipped)
50 }
51
52 rawFM := input[m[4]:m[5]]
53 body = input[m[1]:] // full match length
54 fm, err := parseFrontMatter([]byte(rawFM))
55 if err != nil {
56 log.Fatal(err)
57 }
58
59 mdf := &MDFile{
60 FM: fm,
61 Filename: file,
62 BaseDir: prefix,
63 Body: body,
64 RawFM: rawFM,
65 PendingWrite: false,
66 BaseURL: baseURL,
67 }
68
69 if mdf.Title() == "" || mdf.URL() == "" {
70 return nil, fmt.Errorf("%w: no title or url in frontmatter: %s", ErrFileSkipped, file)
71 }
72 if mdf.Date().IsZero() {
73 return nil, fmt.Errorf("%w: date not found or empty: %s", ErrFileSkipped, file)
74 }
75 return mdf, nil
76}
77
78func parseFrontMatter(data []byte) (map[string]any, error) {
79 d := map[string]any{}
80 err := yaml.Unmarshal(data, &d)
81 if err != nil {
82 return nil, err
83 }
84 return d, nil
85}
86
87func (mdf *MDFile) WriteTo(w io.Writer) (int64, error) {
88 var content strings.Builder
89
90 content.WriteString("---\n")
91
92 yamlData, err := yaml.Marshal(mdf.FM)
93 if err != nil {
94 return 0, err
95 }
96 content.Write(yamlData)
97 content.WriteString("---\n")
98
99 content.WriteString(mdf.Body)
100
101 n, err := io.WriteString(w, content.String())
102 if err == nil {
103 mdf.PendingWrite = false
104 }
105
106 return int64(n), err
107}
108
109func (mdf *MDFile) SetSocial(key, url string) {
110 // Existing
111 social, ok := mdf.FM["social"].(map[string]string)
112 if !ok {
113 social = map[string]string{}
114 }
115
116 social[key] = url
117 mdf.FM["social"] = social
118 mdf.PendingWrite = true
119}
120
121func (mdf *MDFile) GetSocial(key string) string {
122 social, ok := mdf.FM["social"].(map[string]any)
123 if !ok {
124 return ""
125 }
126 val, ok := social[key].(string)
127 if !ok {
128 return ""
129 }
130 return val
131}
132
133func (mdf *MDFile) URL() string {
134 slug, ok := mdf.FM["slug"].(string)
135 if ok {
136 purl, _ := url.JoinPath(mdf.BaseURL, slug)
137 return purl
138 }
139 // If no slug, use filename, without extension
140 name := mdf.Filename
141 ext := filepath.Ext(name)
142 name = strings.TrimSuffix(name, ext)
143 purl, _ := url.JoinPath(mdf.BaseURL, name)
144 return purl
145}
146
147func (mdf *MDFile) Date() time.Time {
148 datestr, ok := mdf.FM["date"].(string)
149 if ok {
150 d, err := time.Parse(time.RFC3339, datestr)
151 if err == nil {
152 return d
153 }
154 }
155 return time.Time{}
156}
157
158func (mdf *MDFile) Title() string {
159 title, ok := mdf.FM["title"].(string)
160 if ok {
161 return title
162 }
163 return ""
164
165}
166
167func (mdf *MDFile) CoverImage() string {
168 if val, ok := mdf.FM["coverImage"].(string); ok {
169 return val
170 }
171 return ""
172}
173
174func (mdf *MDFile) Tags() []string {
175 if val, ok := mdf.FM["tags"].([]string); ok {
176 return val
177 }
178 return []string{}
179
180}
181
182func (mdf *MDFile) Description() string {
183 if val, ok := mdf.FM["description"].(string); ok {
184 return val
185 }
186 return ""
187}
188
189func (mdf *MDFile) String(key string) string {
190 if val, ok := mdf.FM[key].(string); ok {
191 return val
192 }
193 return ""
194}