this repo has no description
at master 238 lines 6.0 kB view raw
1package main 2 3import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io" 8 "io/fs" 9 "os" 10 "path/filepath" 11 "slices" 12 "strings" 13 "time" 14 15 "github.com/briandowns/spinner" 16 "go.seankhliao.com/mono/webstyle" 17 "maragu.dev/gomponents" 18 "maragu.dev/gomponents/html" 19) 20 21const ( 22 singleKey = ":single" 23) 24 25func stripTitles(src []byte) (page []byte, title, subtitle string) { 26 buf := new(bytes.Buffer) 27 sc := bufio.NewScanner(bytes.NewReader(src)) 28 for sc.Scan() { 29 b := sc.Bytes() 30 switch { 31 case bytes.HasPrefix(b, []byte("# ")): 32 title = string(b[2:]) 33 case bytes.HasPrefix(b, []byte("## ")): 34 subtitle = string(b[3:]) 35 default: 36 buf.Write(b) 37 buf.WriteRune('\n') 38 } 39 } 40 page = buf.Bytes() 41 return page, title, subtitle 42} 43 44func renderSingle(in string, compact bool) (map[string]*bytes.Buffer, error) { 45 b, err := os.ReadFile(in) 46 if err != nil { 47 return nil, fmt.Errorf("read file: %w", err) 48 } 49 b, title, subtitle := stripTitles(b) 50 rawHTML, rawCSS, err := webstyle.Markdown(b) 51 if err != nil { 52 return nil, fmt.Errorf("parse markdown: %w", err) 53 } 54 buf := new(bytes.Buffer) 55 o := webstyle.NewOptions( 56 title, 57 subtitle, 58 []gomponents.Node{gomponents.Raw(string(rawHTML))}) 59 o.CustomCSS = string(rawCSS) 60 o.CompactStyle = compact 61 err = webstyle.Structured(buf, o) 62 if err != nil { 63 return nil, fmt.Errorf("render result: %w", err) 64 } 65 return map[string]*bytes.Buffer{singleKey: buf}, nil 66} 67 68func renderMulti(in, gtm, baseURL string, compact bool) (map[string]*bytes.Buffer, error) { 69 var countFiles int 70 fsys := os.DirFS(in) 71 err := fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error { 72 if err != nil || d.IsDir() { 73 return err 74 } 75 countFiles++ 76 return nil 77 }) 78 if err != nil { 79 return nil, fmt.Errorf("walk source: %w", err) 80 } 81 spin := spinner.New(spinner.CharSets[39], 100*time.Millisecond) 82 spin.Start() 83 defer spin.Stop() 84 85 rendered := make(map[string]*bytes.Buffer) 86 rendered["sitemap.txt"] = new(bytes.Buffer) 87 err = fs.WalkDir(fsys, ".", walk(fsys, spin, rendered, gtm, baseURL, compact)) 88 if err != nil { 89 return nil, fmt.Errorf("process source: %w", err) 90 } 91 92 spin.FinalMSG = fmt.Sprintf("%3d rendered pages\n", len(rendered)) 93 94 return rendered, nil 95} 96 97func walk(fsys fs.FS, spin *spinner.Spinner, rendered map[string]*bytes.Buffer, gtm, baseURL string, compact bool) fs.WalkDirFunc { 98 var idx int 99 return func(p string, d fs.DirEntry, openErr error) error { 100 if openErr != nil || d.IsDir() { 101 return openErr 102 } 103 104 idx++ 105 spin.Suffix = fmt.Sprintf("%3d processing %q", idx, p) 106 107 inFile, openErr := fsys.Open(p) 108 if openErr != nil { 109 return fmt.Errorf("open file: %w", openErr) 110 } 111 defer inFile.Close() 112 113 buf := new(bytes.Buffer) 114 if strings.HasSuffix(p, ".draft.md") { 115 // skip drafts 116 return nil 117 } 118 if strings.HasSuffix(p, ".md") { 119 b, err := io.ReadAll(inFile) 120 if err != nil { 121 return fmt.Errorf("read file: %w", err) 122 } 123 b, title, subtitle := stripTitles(b) 124 rawHTML, rawCSS, err := webstyle.Markdown(b) 125 if err != nil { 126 return fmt.Errorf("render markdown: %w", err) 127 } 128 129 u := baseURL + canonicalPathFromRelPath(p) 130 o := webstyle.NewOptions( 131 subtitle, 132 title, 133 []gomponents.Node{gomponents.Raw(string(rawHTML))}, 134 ) 135 o.CompactStyle = compact 136 o.CanonicalURL = u 137 o.CustomCSS = string(rawCSS) 138 139 if title == "" { 140 return fmt.Errorf("missing title") 141 } 142 143 if p == "index.md" { // root index 144 o.HideTitles = true 145 } else if strings.HasSuffix(p, "/index.md") { // all other directory indexes 146 var list gomponents.Node 147 list, err = directoryList(fsys, p) 148 if err != nil { 149 return err 150 } 151 o.Content = append(o.Content, list) 152 } 153 154 err = webstyle.Structured(buf, o) 155 if err != nil { 156 return fmt.Errorf("render: %w", err) 157 } 158 159 fmt.Fprintf(rendered["sitemap.txt"], "%s\n", u) 160 p = p[:len(p)-3] + ".html" 161 } else if strings.HasSuffix(p, ".table.cue") { 162 u := baseURL + canonicalPathFromRelPath(p) 163 openErr = processTable(buf, inFile, u, gtm) 164 if openErr != nil { 165 return fmt.Errorf("process table: %w", openErr) 166 } 167 fmt.Fprintf(rendered["sitemap.txt"], "%s\n", u) 168 p = p[:len(p)-len(".table.cue")] + ".html" 169 } else if strings.HasSuffix(p, ".events.cue") { 170 u := baseURL + canonicalPathFromRelPath(p) 171 openErr = processEvents(buf, inFile, u, gtm) 172 if openErr != nil { 173 return fmt.Errorf("process events: %w", openErr) 174 } 175 fmt.Fprintf(rendered["sitemap.txt"], "%s\n", u) 176 p = p[:len(p)-len(".events.cue")] + ".html" 177 } else { 178 _, openErr = io.Copy(buf, inFile) 179 if openErr != nil { 180 return fmt.Errorf("copy: %w", openErr) 181 } 182 } 183 184 rendered[p] = buf 185 186 return nil 187 } 188} 189 190func directoryList(fsys fs.FS, p string) (gomponents.Node, error) { 191 des, err := fs.ReadDir(fsys, filepath.Dir(p)) 192 if err != nil { 193 return nil, fmt.Errorf("read dir: %w", err) 194 } 195 196 // reverse order 197 slices.SortFunc(des, func(a, b fs.DirEntry) int { 198 return strings.Compare(b.Name(), a.Name()) 199 }) 200 201 entries := make([]gomponents.Node, 0, len(des)) 202 for _, de := range des { 203 if de.IsDir() || de.Name() == "index.md" { 204 continue 205 } 206 n := de.Name() // 120XX-YY-ZZ-some-title.md 207 if strings.HasPrefix(n, "120") && len(n) > 15 && n[11] == '-' { 208 entries = append(entries, html.Li( 209 html.Time( 210 html.DateTime(n[1:11]), // 20XX-YY-ZZ 211 gomponents.Text(n[:11]), // 120XX-YY-ZZ 212 ), 213 gomponents.Text(" | "), 214 html.A( 215 html.Href(n[:len(n)-3]+"/"), // 120XX-YY-ZZ-some-title/ 216 gomponents.Text(strings.ReplaceAll(n[12:len(n)-3], "-", " ")), // some title 217 ), 218 )) 219 } 220 } 221 return html.Ul(entries...), nil 222} 223 224func canonicalPathFromRelPath(in string) string { 225 in = strings.TrimSuffix(in, ".md") 226 in = strings.TrimSuffix(in, ".html") 227 in = strings.TrimSuffix(in, ".table.cue") 228 in = strings.TrimSuffix(in, ".events.cue") 229 in = strings.TrimSuffix(in, "index") 230 if in == "" { 231 return "/" 232 } else if in[len(in)-1] == '/' { 233 return "/" + in 234 } else if strings.HasPrefix(in, "static/") { 235 return "/" + in 236 } 237 return "/" + in + "/" 238}