1// ===== cli.go =====
2// Package main cli.go
3package main
4
5import (
6 _ "embed"
7 "fmt"
8 "log"
9 "os"
10 "reflect"
11 "runtime"
12 "strconv"
13 "strings"
14 "time"
15
16 "github.com/0magnet/calvin"
17 "github.com/bitfield/script"
18 cc "github.com/ivanpirog/coloredcobra"
19 "github.com/spf13/cobra"
20 "github.com/stripe/stripe-go/v81"
21 "golang.org/x/text/cases"
22 "golang.org/x/text/language"
23)
24
25func init() {
26 stripe.EnableTelemetry = false
27 rootCmd.CompletionOptions.DisableDefaultCmd = true
28 rootCmd.AddCommand(
29 runCmd,
30 genCmd,
31 wasmCmd,
32 )
33 var helpflag bool
34 rootCmd.SetUsageTemplate(help)
35 rootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for "+rootCmd.Use)
36 rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
37 rootCmd.PersistentFlags().MarkHidden("help") //nolint
38
39}
40
41var rootCmd = &cobra.Command{
42 Use: "m2",
43 Short: "web store server",
44 Long: calvin.AsciiFont("magnetosphere.net") + "\n" + "web store server",
45}
46
47var genCmd = &cobra.Command{
48 Use: "gen",
49 Short: "generate conf template",
50 Long: "generate conf template",
51 Run: func(_ *cobra.Command, _ []string) {
52 fmt.Println(envfiletemplate)
53 },
54}
55
56// Execute executes the root cli command
57func Execute() {
58 cc.Init(&cc.Config{
59 RootCmd: rootCmd,
60 Headings: cc.HiBlue + cc.Bold,
61 Commands: cc.HiBlue + cc.Bold,
62 CmdShortDescr: cc.HiBlue,
63 Example: cc.HiBlue + cc.Italic,
64 ExecName: cc.HiBlue + cc.Bold,
65 Flags: cc.HiBlue + cc.Bold,
66 FlagsDescr: cc.HiBlue,
67 NoExtraNewlines: true,
68 NoBottomNewline: true,
69 })
70 if err := rootCmd.Execute(); err != nil {
71 log.Fatal("Failed to execute command: ", err)
72 }
73}
74
75var menvfile = os.Getenv("MENV")
76
77type flagVars struct {
78 Teststripekey bool
79 ProductsCSV string
80 WebPort int
81 CoreRunWebPort int
82 StripelivePK string
83 StripeliveSK string
84 StripetestPK string
85 StripetestSK string
86 StripeSK string
87 StripePK string
88 Siteimagesrc string
89 Siteordersurl string
90 Sitename string
91 Siteext string
92 Sitedomain string
93 Sitelongname string
94 Sitetagline string
95 Sitemeta string
96 Siteprettyname string
97 Siteprettynamecap string
98 Siteprettynamecaps string
99 SiteASCIILogo string
100 Tgcontact string
101 Tgchannel string
102 UseTinygo bool
103 WasmSRC []string
104 WasmExecPath string
105 WasmExecPathGo string
106 WasmExecPathTinyGo string
107 Gobuild string
108 Tinygobuild string
109 Buildwasmwith string
110 LDFlagsX string
111 PrinterName string // CUPS queue name (blank = default)
112 CupsOptions string // comma-separated -o options
113 LpTimeout time.Duration // timeout for `lp`
114}
115
116var f = flagVars{
117 // WasmSRC: []string{"wasm/stl2.go","wasm/checkout_wasm.go"},
118 WasmExecPath: runtime.GOROOT() + "/lib/wasm/wasm_exec.js", //nolint
119 WasmExecPathGo: runtime.GOROOT() + "/lib/wasm/wasm_exec.js", //nolint
120 WasmExecPathTinyGo: strings.TrimSuffix(runtime.GOROOT(), "go") + "tinygo" + "/targets/wasm_exec.js", //nolint
121 Gobuild: "go build",
122 Tinygobuild: "tinygo build -target=wasm --no-debug",
123 Buildwasmwith: "go build",
124 LDFlagsX: "stripePK",
125}
126
127var (
128 // Hardcoded array of valid shorthand characters, excluding "h"
129 shorthandChars = []rune("abcdefgijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
130 nextShortIndex = 0 // Index for the next shorthand flag
131)
132
133// Get the next available shorthand flag
134func getNextShortFlag() string {
135 if nextShortIndex >= len(shorthandChars) {
136 return ""
137 }
138 short := shorthandChars[nextShortIndex]
139 nextShortIndex++
140 return string(short)
141}
142
143var a = true
144var b = false
145
146func addStringFlag(cmds []*cobra.Command, f interface{}, fieldPtr *string, description string) {
147 for i, _ := range cmds {
148 cmds[i].Flags().StringVarP(fieldPtr, ccc(fieldPtr, f, b), getNextShortFlag(), scriptExecString(fmt.Sprintf("${%s%s}", ccc(fieldPtr, f, a), func(s string) string {
149 if s != "" {
150 s = "-" + s
151 }
152 return s
153 }(*fieldPtr))), fmt.Sprintf("%s env: %s\033[0m\n\r", description, ccc(fieldPtr, f, a)))
154 }
155}
156
157func addStringSliceFlag(cmds []*cobra.Command, f interface{}, fieldPtr *[]string, description string) {
158 for i, _ := range cmds {
159 cmds[i].Flags().StringSliceVarP(
160 fieldPtr,
161 ccc(fieldPtr, f, b),
162 getNextShortFlag(),
163 scriptExecStringSlice(fmt.Sprintf("${%s[@]}", ccc(fieldPtr, f, a))),
164 fmt.Sprintf("%s env: %s\033[0m\n\r", description, ccc(fieldPtr, f, a)),
165 )
166 }
167}
168
169func addBoolFlag(cmds []*cobra.Command, f interface{}, fieldPtr *bool, description string) {
170 for i, _ := range cmds {
171 cmds[i].Flags().BoolVarP(fieldPtr, ccc(fieldPtr, f, b), getNextShortFlag(), scriptExecBool(fmt.Sprintf("${%s%s}", ccc(fieldPtr, f, a), func(b bool) string {
172 return "-" + strconv.FormatBool(b)
173 }(*fieldPtr))), fmt.Sprintf("%s env: %s\033[0m\n\r", description, ccc(fieldPtr, f, a)))
174 }
175}
176func addIntFlag(cmds []*cobra.Command, f interface{}, fieldPtr *int, description string) {
177 for i, _ := range cmds {
178 cmds[i].Flags().IntVarP(fieldPtr, ccc(fieldPtr, f, b), getNextShortFlag(), scriptExecInt(fmt.Sprintf("${%s%s}", ccc(fieldPtr, f, a), func(i int) string {
179 return fmt.Sprintf("-%d", i)
180 }(*fieldPtr))), fmt.Sprintf("%s env: %s\033[0m\n\r", description, ccc(fieldPtr, f, a)))
181 }
182}
183
184func addDurationFlag(cmds []*cobra.Command, f interface{}, fieldPtr *time.Duration, description string) {
185 for i := range cmds {
186 // Keep parity with your pattern of embedding a "-" when a non-zero default is present.
187 def := scriptExecDuration(fmt.Sprintf("${%s%s}",
188 ccc(fieldPtr, f, a),
189 func(d time.Duration) string {
190 if d != 0 {
191 return "-" + d.String() // e.g. "-5s"
192 }
193 return ""
194 }(*fieldPtr),
195 ))
196 cmds[i].Flags().DurationVarP(
197 fieldPtr,
198 ccc(fieldPtr, f, b),
199 getNextShortFlag(),
200 def,
201 fmt.Sprintf("%s env: %s\033[0m\n\r", description, ccc(fieldPtr, f, a)),
202 )
203 }
204}
205
206func init() {
207 runCmd.Flags().SortFlags = false
208 addStringFlag([]*cobra.Command{runCmd}, &f, &f.ProductsCSV, "products csv file")
209 addBoolFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.Teststripekey, "use stripe test api keys instead of live key")
210 addStringFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.StripeliveSK, "stripe live api sk")
211 addStringFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.StripelivePK, "stripe live api pk")
212 addStringFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.StripetestSK, "stripe test api sk")
213 addStringFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.StripetestPK, "stripe test api pk")
214 addIntFlag([]*cobra.Command{runCmd}, &f, &f.WebPort, "port to serve on")
215 addStringFlag([]*cobra.Command{runCmd}, &f, &f.Siteimagesrc, "domain for images - leave blank to serve images")
216 addStringFlag([]*cobra.Command{runCmd}, &f, &f.Siteordersurl, "domain for orders - leave blank for same domain")
217 addStringFlag([]*cobra.Command{runCmd}, &f, &f.Sitename, "site name")
218 addStringFlag([]*cobra.Command{runCmd}, &f, &f.Siteext, "site domain extension")
219 addStringFlag([]*cobra.Command{runCmd}, &f, &f.Sitelongname, "site long name")
220 addStringFlag([]*cobra.Command{runCmd}, &f, &f.Sitetagline, "site tag line")
221 addStringFlag([]*cobra.Command{runCmd}, &f, &f.Sitemeta, "site meta")
222 addStringFlag([]*cobra.Command{runCmd}, &f, &f.Tgcontact, "telegram contact")
223 addStringFlag([]*cobra.Command{runCmd}, &f, &f.Tgchannel, "telegram channel")
224 addBoolFlag([]*cobra.Command{runCmd}, &f, &f.UseTinygo, "use tinygo instead of go to compile wasm")
225 addStringSliceFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.WasmSRC, "wasm source code files RELATIVE PATHS without '..'")
226 addStringFlag([]*cobra.Command{runCmd}, &f, &f.PrinterName, "CUPS printer name (default: system default)")
227 addStringFlag([]*cobra.Command{runCmd}, &f, &f.CupsOptions, "e.g. 'media=Custom.80x200mm,fit-to-page'")
228 addDurationFlag([]*cobra.Command{runCmd}, &f, &f.LpTimeout, "timeout for lp command")
229
230}
231
232// change case
233func ccc(val interface{}, strct interface{}, upper bool) string {
234 v := reflect.ValueOf(strct)
235 if v.Kind() == reflect.Ptr {
236 v = v.Elem()
237 }
238 if v.Kind() != reflect.Struct {
239 panic("uc: second argument must be a pointer to a struct")
240 }
241 for i := 0; i < v.NumField(); i++ {
242 field := v.Field(i)
243 if field.CanAddr() && field.Addr().Interface() == val {
244 if upper {
245 return strings.ToUpper(v.Type().Field(i).Name)
246 }
247 return strings.ToLower(v.Type().Field(i).Name)
248 }
249 }
250 return ""
251}
252
253func initstripePK() {
254 f.StripeSK = f.StripeliveSK
255 f.StripePK = f.StripelivePK
256 if f.Teststripekey {
257 f.StripeSK = f.StripetestSK
258 f.StripePK = f.StripetestPK
259 }
260 stripe.Key = f.StripeSK
261 // awkward way to do this
262 f.LDFlagsX += "=" + f.StripePK
263}
264
265var wasmCmd = &cobra.Command{
266 Use: "wasm",
267 Short: "compile wasm",
268 Long: "compile wasm",
269 Run: func(_ *cobra.Command, _ []string) {
270 if len(f.WasmSRC) == 0 {
271 log.Fatal("No wasm source code specified")
272 }
273 initstripePK()
274 compileWASM()
275 },
276}
277
278var runCmd = &cobra.Command{
279 Use: "run",
280 Short: "run the web application",
281 Long: calvin.AsciiFont("magnetosphere.net") + "\n" + func() string {
282 helptext := `Run the web application
283Generate a config file first
284
285Config defaults file may also be specified with:
286MENV=m2.conf m2 run
287OR
288MENV=/path/to/m2.conf m2 run
289print the MENV file template with:
290m2 gen`
291 if menvfile == "" {
292 return helptext
293 }
294 if _, err := os.Stat(menvfile); err == nil {
295 return `Run the web application
296
297menv file detected: ` + menvfile
298 }
299 return helptext
300 }(),
301 Run: func(_ *cobra.Command, _ []string) {
302 f.Sitedomain = f.Sitename + f.Siteext
303 log.Println(" Initializing " + f.Sitedomain)
304 fmt.Println(calvin.BlackboardBold(f.Sitedomain))
305 fmt.Println(calvin.AsciiFont(f.Sitedomain))
306 initstripePK()
307 f.Siteprettyname = calvin.BlackboardBold(f.Sitedomain) //"ππππππ₯π π€π‘πππ£π.πππ₯"
308 c := cases.Title(language.English)
309 f.Siteprettynamecap = calvin.BlackboardBold(c.String(f.Sitedomain)) //"ππππππ₯π π€π‘πππ£π.πππ₯"
310 f.Siteprettynamecaps = calvin.BlackboardBold(strings.ToUpper(f.Sitedomain)) //"ππΈπΎβπΌπππββπΌβπΌ.βπΌπ"
311 f.SiteASCIILogo = strings.Replace(strings.Replace(calvin.AsciiFont(f.Sitedomain), " ", " ", -1), "\n", "<br>\n", -1)
312
313 if f.UseTinygo {
314 f.WasmExecPath = f.WasmExecPathTinyGo
315 f.Buildwasmwith = f.Tinygobuild
316 }
317 if len(f.WasmSRC) == 0 {
318 f.WasmExecPath = ""
319 f.Buildwasmwith = ""
320 }
321 log.Println("Checking for products CSV")
322 fileInfo, err := os.Stat(f.ProductsCSV)
323 if err != nil {
324 log.Fatal("Error getting file info:", err)
325 return
326 }
327 lastModTime = fileInfo.ModTime()
328 log.Println("Reading products CSV")
329 allproducts = readCSV(f.ProductsCSV)
330 go func() {
331 for {
332 fileInfo, err := os.Stat(f.ProductsCSV)
333 if err != nil {
334 log.Println("Error getting file info:", err)
335 }
336
337 currentModTime := fileInfo.ModTime()
338 if currentModTime != lastModTime {
339 log.Println("CSV file has been modified!")
340 allproducts = readCSV(f.ProductsCSV)
341 lastModTime = currentModTime
342 createCORE()
343 buildCORE()
344 }
345
346 time.Sleep(10 * time.Second)
347 }
348 }()
349
350 server()
351 },
352}
353
354var lastModTime time.Time
355
356func scriptExecString(s string) string {
357 z, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; printf "%s"'`, menvfile, s)).String()
358 if err == nil {
359 return strings.TrimSpace(z)
360 }
361 return ""
362}
363
364func scriptExecStringSlice(s string) []string {
365 z, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; printf "%s" "%s"'`, menvfile, "%s\n", s)).Slice()
366 if err == nil {
367 return z
368 }
369 return []string{""}
370}
371
372func scriptExecBool(s string) bool {
373 z, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; printf "%s"'`, menvfile, s)).String()
374 if err == nil {
375 b, err := strconv.ParseBool(z)
376 if err == nil {
377 return b
378 }
379 }
380 return false
381}
382
383/*
384func scriptExecArray(s string) string {
385 y, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; for _i in %s ; do echo "$_i" ; done'`, menvfile, s)).Slice()
386 if err == nil {
387 return strings.Join(y, ",")
388 }
389 return ""
390}
391*/
392
393func scriptExecInt(s string) int {
394 z, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; printf "%s"'`, menvfile, s)).String()
395 if err == nil {
396 if z == "" {
397 return 0
398 }
399 i, err := strconv.Atoi(z)
400 if err == nil {
401 return i
402 }
403 }
404 return 0
405}
406
407// Accepts Go duration strings ("750ms", "2s", "5m", "1h").
408// Also accepts a bare integer (treated as seconds).
409// If the evaluated string starts with "-", it is trimmed (matching your other helpersβ default building).
410func scriptExecDuration(s string) time.Duration {
411 z, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; printf "%s"'`, menvfile, s)).String()
412 if err != nil {
413 return 0
414 }
415 z = strings.TrimSpace(z)
416 if z == "" {
417 return 0
418 }
419 z = strings.TrimPrefix(z, "-") // keep parity with how defaults are built
420
421 // Try full Go duration syntax first.
422 if d, err := time.ParseDuration(z); err == nil {
423 return d
424 }
425 // Fallback: plain integer means seconds.
426 if n, err := strconv.ParseInt(z, 10, 64); err == nil {
427 return time.Duration(n) * time.Second
428 }
429 return 0
430}
431
432const help = "\r\n" +
433 " {{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" +
434 "{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" +
435 "Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n " +
436 "{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" +
437 "Flags:\r\n" +
438 "{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" +
439 "Global Flags:\r\n" +
440 "{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n"
441
442const envfiletemplate = `#
443# /etc/m2.conf
444#
445#########################################################################
446# M2 CONFIG TEMPLATE
447# change config defaults
448# or comment values with # to exclude
449#########################################################################
450
451### Stripe Configuration ################################################
452
453#-- Live and test API keys - REQUIRED
454STRIPELIVEPK='pk_live_...'
455STRIPELIVESK='sk_live_...'
456STRIPETESTPK='pk_test_...'
457STRIPETESTSK='sk_test_...'
458
459#-- Use Test Keys
460USETESTKEY=true
461
462### Site Product Data Configuration #####################################
463
464#-- Products CSV path (ex. 'products.csv')
465PRODUCTSCVS=''
466
467#-- Image subdomain (ex. 'https://img.magnetosphere.net')
468# no trailing slash '/' !
469IMGSRC=''
470
471#-- Orders subdomain (ex. 'https://pay.magnetosphere.net')
472# no trailing slash '/' !
473ORDERSURL=''
474
475### Site Configuration ##################################################
476
477#-- Website (Host) Name (domain minus extension - ex. 'magnetosphere')
478SITENAME=''
479
480#-- Website Domain Extension (ex. '.com' '.net')
481SITEEXT=''
482
483#-- Site Long Name (ex. 'magnetosphere electronic surplus')
484SITELONGNAME=''
485
486#-- Site Tag Line (ex. 'we have the technology')
487SITETAGLINE=''
488
489#-- Site Meta Description (ex. 'we have the technology (ββΏβ) electronic surplus for sale')
490SITEMETA=''
491
492#-- Site Telegram Contact
493# DO NOT INCLUDE 'https://t.me/'
494# ex. 'magnetosphere' will display on-site as 'https://t.me/magnetosphere'
495TGCONTACT=''
496
497#-- Site Telegram Channel
498# DO NOT INCLUDE 'https://t.me/'
499# ex. 'magnetospheredotnet' will display on-site as 'https://t.me/magnetospheredotnet'
500TGCHANNEL=''
501
502### Web Server Configuration ############################################
503
504#-- Port to serve http on (ex. '9883')
505WEBPORT='9883'
506
507#-- CUPS printer name (default: system default)
508PRINTERNAME=''
509
510#-- CUPS options, comma separated (e.g. 'media=Custom.80x200mm,fit-to-page')
511CUPSOPTIONS=''
512
513#-- timeout for lp command
514LPTIMEOUT="10s"
515`
516
517
518// ===== core.go =====
519// Package main core.go
520package main
521
522import (
523 "bufio"
524 "bytes"
525 "encoding/json"
526 htmpl "html/template"
527 "log"
528 "os"
529 "path/filepath"
530 "sort"
531 "strings"
532 ttmpl "text/template"
533 "time"
534
535 p "github.com/0magnet/m2/pkg/product"
536 "github.com/bitfield/script"
537 "github.com/briandowns/spinner"
538 "github.com/gofiber/fiber/v3"
539)
540
541func watchCORE() {
542 createCORE()
543 buildCORE()
544
545 files := map[string]struct {
546 lastMod time.Time
547 handler func()
548 }{
549 "ui/π.go": {
550 handler: func() {
551 createCORE()
552 buildCORE()
553 },
554 },
555 "htmpl/product.md": {
556 handler: func() {
557 createCORE()
558 buildCORE()
559 },
560 },
561 }
562
563 go func() {
564 for path := range files {
565 fi, err := os.Stat(path)
566 if err != nil {
567 log.Printf("Cannot stat %s: %v", path, err)
568 continue
569 }
570 files[path] = struct {
571 lastMod time.Time
572 handler func()
573 }{
574 lastMod: fi.ModTime(),
575 handler: files[path].handler,
576 }
577 }
578
579 ticker := time.NewTicker(5 * time.Second)
580 defer ticker.Stop()
581
582 for range ticker.C {
583 for path, entry := range files {
584 fi, err := os.Stat(path)
585 if err != nil {
586 log.Printf("Error stating %s: %v", path, err)
587 continue
588 }
589 if fi.ModTime().After(entry.lastMod) {
590 log.Println("π¦ Detected change in", path)
591 files[path] = struct {
592 lastMod time.Time
593 handler func()
594 }{
595 lastMod: fi.ModTime(),
596 handler: entry.handler,
597 }
598 entry.handler()
599 }
600 }
601 }
602 }()
603}
604
605func handleCORE(r *fiber.App) {
606 r.Use("/", func(c fiber.Ctx) error {
607 coreUIPath := "ui/bin/web"
608 trim := strings.Trim(c.Path(), "/")
609 if trim == "" {
610 trim = "index.html"
611 }
612 fullPath := filepath.Join(coreUIPath, trim)
613 info, err := os.Stat(fullPath)
614 if err == nil && info.IsDir() {
615 fullPath = filepath.Join(fullPath, "index.html")
616 }
617 if _, err := os.Stat(fullPath); err != nil {
618 c.Status(fiber.StatusNotFound)
619 return c.SendFile(filepath.Join(coreUIPath, "404.html"))
620 }
621 if strings.HasSuffix(fullPath, ".wasm") {
622 c.Set("Content-Type", "application/wasm")
623 }
624 return c.SendFile(fullPath)
625 })
626}
627
628func buildCORE() {
629 s := spinner.New(spinner.CharSets[14], 25*time.Millisecond)
630 s.Suffix = " Building C.O.R.E UI..."
631 s.Start()
632 _, err := script.Exec(`bash -c 'set -x ; go get -u ./... ; go mod tidy ; go mod vendor ; cd ui || exit 1 ; rm -rf bin ; time go run -x cogentcore.org/core@main build web -vv -ldflags="-X 'main.` + f.LDFlagsX + `'" || timeout 30 go run -x .'`).Stdout()
633 s.Stop()
634 if err != nil {
635 log.Println(err)
636 } else {
637 log.Println("β
Done building C.O.R.E UI")
638 }
639}
640
641func createCORE() {
642 log.Println("Creating Content Files")
643 if _, err := script.Exec(`bash -c 'rm -rf ui/content ; cp -r ui/content-bak ui/content'`).Stdout(); err != nil {
644 log.Fatalf(err.Error())
645 }
646 log.Println("Populating Content")
647 prodPageMDTmpl, err := ttmpl.New("index").Funcs(htmpl.FuncMap{
648 "replace": replace, "mul": mul, "div": div,
649 "safeHTML": safeHTML, "safeJS": safeJS, "stripProtocol": stripProtocol,
650 "add": add, "sub": sub, "toFloat": toFloat, "equalsIgnoreCase": equalsIgnoreCase,
651 "getsubcats": getsubcats, "escapesubcat": escapesubcat,
652 "sortsubcats": sortsubcats, "repeat": repeat,
653 }).Parse(h.ProductPageMD())
654 if err != nil {
655 log.Println("Error parsing product page markdown template:", err)
656 log.Fatalf(err.Error())
657 }
658
659 catPageMDTmpl, err := ttmpl.New("index").Funcs(htmpl.FuncMap{
660 "replace": replace, "mul": mul, "div": div,
661 "safeHTML": safeHTML, "safeJS": safeJS, "stripProtocol": stripProtocol,
662 "add": add, "sub": sub, "toFloat": toFloat, "equalsIgnoreCase": equalsIgnoreCase,
663 "getsubcats": getsubcats, "escapesubcat": escapesubcat,
664 "sortsubcats": sortsubcats, "repeat": repeat,
665 }).Parse(h.CategoryPageMD())
666 if err != nil {
667 log.Println("Error parsing category page markdown template:", err)
668 log.Fatalf(err.Error())
669 }
670
671 var enabled []p.Product
672 catSet := make(map[string]struct{})
673 for _, prod := range allproducts {
674 if strings.EqualFold(prod.Enable, "TRUE") {
675 enabled = append(enabled, prod)
676 catSet[prod.Category] = struct{}{}
677 }
678 }
679
680 var cats []string
681 for c := range catSet {
682 cats = append(cats, c)
683 }
684 sort.Slice(cats, func(i, j int) bool {
685 return strings.ToLower(cats[i]) < strings.ToLower(cats[j])
686 })
687
688 for _, cat := range cats {
689 var prods []p.Product
690 for _, prod := range enabled {
691 if strings.EqualFold(prod.Category, cat) {
692 prods = append(prods, prod)
693 }
694 }
695
696 var buf bytes.Buffer
697 if err := catPageMDTmpl.Execute(&buf, map[string]interface{}{
698 "Products": prods,
699 "Category": cat,
700 "Domain": f.Sitedomain,
701 "Page": map[string]interface{}{
702 "ImgSRC": f.Siteimagesrc,
703 },
704 }); err != nil {
705 log.Fatalf("execute category MD (%q): %v", cat, err)
706 }
707
708 lines := strings.Split(buf.String(), "\n")
709 var cleaned []string
710 for _, line := range lines {
711 if strings.TrimSpace(line) != "" {
712 cleaned = append(cleaned, line)
713 }
714 }
715 md := strings.Join(cleaned, "\n") + "\n"
716
717 filename := "ui/content/" + escapesubcat(cat) + ".md"
718 if _, err := script.Echo(md).WriteFile(filename); err != nil {
719 log.Fatalf("write category MD (%q): %v", filename, err)
720 }
721 }
722
723 for _, product := range allproducts {
724 if product.Enable != "TRUE" {
725 continue
726 }
727
728 var buf bytes.Buffer
729 err := prodPageMDTmpl.Execute(&buf, map[string]interface{}{"Prod": product, "Domain": f.Sitedomain})
730 if err != nil {
731 log.Fatalf(err.Error())
732 }
733 lines := strings.Split(buf.String(), "\n")
734 var cleanedLines []string
735 for _, line := range lines {
736 if strings.TrimSpace(line) != "" {
737 cleanedLines = append(cleanedLines, line)
738 }
739 }
740 cleaned := strings.Join(cleanedLines, "\n") + "\n"
741 filename := "ui/content/" + escapesubcat(product.Partno) + ".md"
742 _, err = script.Echo(cleaned).WriteFile(filename)
743 if err != nil {
744 log.Fatalf(err.Error())
745 }
746 }
747 log.Println("Writing products.json")
748
749 data := readproductscsv(f.ProductsCSV)
750 if len(data) == 0 {
751 log.Fatalf("CSV file %s is empty or unreadable", f.ProductsCSV)
752 }
753 scanner := bufio.NewScanner(bytes.NewReader(data))
754 var jsonData []map[string]string
755 var headers []string
756 lineNum := 0
757 for scanner.Scan() {
758 line := scanner.Text()
759 f := strings.Split(line, ",")
760 if lineNum == 0 {
761 headers = f
762 lineNum++
763 continue
764 }
765 if len(f) < 4 {
766 continue
767 }
768 if f[3] == "TRUE" {
769 entry := make(map[string]string)
770 for i := range f {
771 if i < len(headers) {
772 entry[headers[i]] = f[i]
773 }
774 }
775 jsonData = append(jsonData, entry)
776 }
777 lineNum++
778 }
779
780 if err := scanner.Err(); err != nil {
781 log.Fatalf("Error scanning CSV: %v", err)
782 }
783
784 // Convert to JSON
785 output, err := json.MarshalIndent(jsonData, "", " ")
786 if err != nil {
787 log.Fatalf("Error encoding JSON: %v", err)
788 }
789
790 _, err = script.Echo(string(output)).WriteFile("ui/products.json")
791 if err != nil {
792 log.Fatalf(err.Error())
793 }
794}
795
796
797// ===== csv.go =====
798// Package main csv.go
799package main
800
801import (
802 "bufio"
803 "bytes"
804 _ "embed"
805 "log"
806 "strings"
807
808 p "github.com/0magnet/m2/pkg/product"
809 "github.com/bitfield/script"
810)
811
812var allproducts p.Products
813
814func readproductscsv(csvFile string) (data []byte) {
815 data, err := script.File(csvFile).Bytes() //nolint
816 if err != nil {
817 log.Printf(`Error reading %s file %v`, csvFile, err)
818 }
819 return data
820}
821
822func readCSV(csvFile string) (prods p.Products) {
823 scanner := bufio.NewScanner(bytes.NewReader(readproductscsv(csvFile)))
824 for scanner.Scan() {
825 line := scanner.Text()
826 f := strings.Split(line, ",")
827 if len(f) < 4 {
828 continue
829 }
830 if f[3] == "TRUE" {
831 q := p.Product{
832 Image1: f[0],
833 Partno: f[1],
834 Name: f[2],
835 Enable: f[3],
836 Price: f[4],
837 Quantity: f[5],
838 Shippable: f[6],
839 Minorder: f[7],
840 Maxorder: f[8],
841 Defaultquantity: f[9],
842 Stepquantity: f[10],
843 Mfgpartno: f[11],
844 Mfgname: f[12],
845 Category: f[13],
846 Subcategory: f[14],
847 Location: f[15],
848 Msrp: f[16],
849 Cost: f[17],
850 Typ: f[18],
851 Packagetype: f[19],
852 Technology: f[20],
853 Materials: f[21],
854 Value: f[22],
855 ValUnit: f[23],
856 Resistance: f[24],
857 ResUnit: f[25],
858 Tolerance: f[26],
859 VoltsRating: f[27],
860 AmpsRating: f[28],
861 WattsRating: f[29],
862 TempRating: f[30],
863 TempUnit: f[31],
864 Description1: f[32],
865 Description2: f[33],
866 Color1: f[34],
867 Color2: f[35],
868 Sourceinfo: f[36],
869 Datasheet: f[37],
870 Docs: f[38],
871 Reference: f[39],
872 Attributes: f[40],
873 Year: f[41],
874 Condition: f[42],
875 Note: f[43],
876 Warning: f[44],
877 CableLengthInches: f[45],
878 LengthInches: f[46],
879 WidthInches: f[47],
880 HeightInches: f[48],
881 WeightLb: f[49],
882 WeightOz: f[50],
883 }
884 prods = append(prods, q)
885 }
886 }
887 return prods
888}
889
890
891// ===== m2.go =====
892// Package main m2.go
893package main
894
895import (
896 "bytes"
897 "encoding/base64"
898 "errors"
899 "fmt"
900 "log"
901 "path/filepath"
902 "regexp"
903 "sort"
904 "strconv"
905 "strings"
906 "sync"
907 "time"
908
909 p "github.com/0magnet/m2/pkg/product"
910 "github.com/bitfield/script"
911 "github.com/gofiber/fiber/v3"
912 "github.com/gofiber/fiber/v3/middleware/static"
913)
914
915
916func main() { Execute() }
917
918var collapseNewlines = regexp.MustCompile(`\n{2,}`)
919
920func methodColor(method string, colors fiber.Colors) string {
921 switch method {
922 case fiber.MethodGet:
923 return colors.Cyan
924 case fiber.MethodPost:
925 return colors.Green
926 case fiber.MethodPut:
927 return colors.Yellow
928 case fiber.MethodDelete:
929 return colors.Red
930 case fiber.MethodPatch:
931 return colors.White
932 case fiber.MethodHead:
933 return colors.Magenta
934 case fiber.MethodOptions:
935 return colors.Blue
936 default:
937 return colors.Reset
938 }
939}
940
941func statusColor(code int, colors fiber.Colors) string {
942 switch {
943 case code >= fiber.StatusOK && code < fiber.StatusMultipleChoices:
944 return colors.Green
945 case code >= fiber.StatusMultipleChoices && code < fiber.StatusBadRequest:
946 return colors.Blue
947 case code >= fiber.StatusBadRequest && code < fiber.StatusInternalServerError:
948 return colors.Yellow
949 default:
950 return colors.Red
951 }
952}
953
954func server() {
955 wg := new(sync.WaitGroup)
956 wg.Add(1)
957 initTMPL()
958 r := fiber.New(fiber.Config{
959 ErrorHandler: func(c fiber.Ctx, err error) error {
960 code := fiber.StatusInternalServerError
961 var e *fiber.Error
962 if errors.As(err, &e) {
963 code = e.Code
964 }
965 c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
966 return c.Status(code).SendString(err.Error())
967 },
968 })
969
970 r.Use(func(c fiber.Ctx) error {
971 start := time.Now()
972 err := c.Next()
973 status := c.Response().StatusCode()
974 lat := time.Since(start)
975 colors := c.App().Config().ColorScheme
976 ip := fmt.Sprintf("%*s", 15, c.IP())
977 ipsStr := strings.Join(c.IPs(), ", ")
978 ips := fmt.Sprintf("%*s", 15, ipsStr)
979 method := fmt.Sprintf("%-*s", 6, c.Method())
980 statCol := statusColor(status, colors) + fmt.Sprintf("%3d", status) + colors.Reset
981 methCol := methodColor(c.Method(), colors) + method + colors.Reset
982 fmt.Printf("%s | %s | %12s | %s | %s | %s | %s\n", time.Now().Format("2006-01-02 15:04:05"), statCol, lat, ip, ips, methCol, c.Path())
983 return err
984 })
985 serveSourceCode(r)
986 serveWASM(r)
987 r.Get("/logo", logo)
988 r.Get("/logo/:width", logo)
989 r.Get("/logo/:width/:height", logo)
990 r.Get("/logo.png", sendFile)
991 r.Get("/logo.html", sendFile)
992 r.Get("/mobilelogo.html", sendFile)
993 r.Get("/logolarge.html", sendFile)
994 r.Get("/favicon.ico", sendImage)
995 r.Get("/robots.txt", robots)
996 if f.Siteimagesrc == "" {
997 r.Use("/i", static.New("./img"))
998 r.Use("/img", static.New("./img"))
999 }
1000 r.Use("/font", static.New("./font"))
1001 r.Get("/stl/:filename", func(c fiber.Ctx) error { return c.SendFile("./img/stl/" + c.Params("filename")) })
1002 r.Get("/stl/base64/:filename", stlbase64)
1003 r.Get("/site.webmanifest", func(c fiber.Ctx) error {
1004 return c.JSON([]byte(`{"name":"","short_name":"","icons":[{"src":"` + f.Siteimagesrc + `/i/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"` + f.Siteimagesrc + `/i/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}`))
1005 })
1006 r.Get("/sitemap", sitemap)
1007 r.Get("/sitemap.xml", sitemap)
1008 r.Get("/coffee", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })
1009 r.Get("/clock", clock)
1010 r.Get("/", homepage)
1011 r.Get("/p/:partno", productpage)
1012 r.Get("/post/:partno", handlecat)
1013 r.Get("/p", handlecat)
1014 r.Get("/cat", handlecat)
1015 r.Get("/cat/:cat", handlecat)
1016 r.Get("/cat/:cat/:subcat", handlecat)
1017 r.Get("/style.css", style)
1018 handleOthers(r)
1019 handleOrder(r)
1020 handleCORE(r)
1021 go func() {
1022 err := r.Listen(fmt.Sprintf(":%d", f.WebPort))
1023 if err != nil {
1024 log.Println("Error serving http: ", err)
1025 }
1026 wg.Done()
1027 }()
1028 watchCORE()
1029 compileWASM()
1030 wg.Wait()
1031}
1032
1033func sitemap(c fiber.Ctx) error {
1034 c.Type("xml", "utf-8")
1035 return c.SendString(generateSitemapXML())
1036}
1037
1038func clock(c fiber.Ctx) error {
1039 c.Set("Content-Type", "text/html;charset=utf-8")
1040 _, err := c.Status(fiber.StatusOK).Write([]byte(h.Clock()))
1041 return err
1042}
1043
1044func logo(c fiber.Ctx) error {
1045 tmpl, err := auxTmpl()
1046 if err != nil {
1047 msg := fmt.Sprintf("Error parsing html template: %v", err)
1048 log.Println(msg)
1049 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1050 }
1051 tmpl0, err := tmpl.Clone()
1052 if err != nil {
1053 msg := fmt.Sprintf("Error cloning template: %v", err)
1054 log.Println(msg)
1055 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1056 }
1057 _, err = tmpl0.New("main").Parse(h.Logo())
1058 if err != nil {
1059 msg := fmt.Sprintf("Error parsing product page template: %v", err)
1060 log.Println(msg)
1061 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1062 }
1063 tmpl = tmpl0
1064 c.Set("Content-Type", "text/html;charset=utf-8")
1065
1066 img2txtFlags := ""
1067 if w, err := strconv.Atoi(c.Params("width")); err == nil {
1068 img2txtFlags = fmt.Sprintf("--width=%d ",w)
1069 }
1070 if h, err := strconv.Atoi(c.Params("height")); err == nil {
1071 img2txtFlags = fmt.Sprintf("--height=%d ",h)
1072 }
1073
1074 logoHTMLslice, err := script.Exec(fmt.Sprintf("bash -c 'img2txt %s logo.jpg | ansifilter -H'", img2txtFlags)).Slice()
1075 if err != nil {
1076 log.Println("error: ", err)
1077 _, err = c.Status(fiber.StatusInternalServerError).Write([]byte(err.Error()+"/n"+strings.Join(logoHTMLslice,"\n")))
1078 return err
1079 }
1080 if len(logoHTMLslice) > 2 {
1081 logoHTMLslice = logoHTMLslice[:len(logoHTMLslice)-3]
1082 }
1083 if len(logoHTMLslice) > 18 {
1084 logoHTMLslice = logoHTMLslice[19:]
1085 }
1086
1087
1088 var result bytes.Buffer
1089 h1 := pageMeta(c, htmlTemplateData{})
1090 h1.Page = "logo"
1091 h1.Title = "logo"
1092 tmplData := map[string]interface{}{
1093 "Content": strings.Join(logoHTMLslice, "\n"),
1094 }
1095 err = tmpl.Execute(&result, tmplData)
1096 if err != nil {
1097 log.Println("error: ", err)
1098 _, err = c.Status(fiber.StatusInternalServerError).Write(result.Bytes())
1099 return err
1100 }
1101 _, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1102 return err
1103}
1104
1105func robots(c fiber.Ctx) error {
1106 c.Set("Content-Type", "text/plain;charset=utf-8")
1107 _, err := c.Status(fiber.StatusOK).Write([]byte(fmt.Sprintf("User-agent: *\n\nSitemap: https://%s/sitemap.xml", c.Hostname())))
1108 return err
1109}
1110
1111func style(c fiber.Ctx) error {
1112 c.Set("Content-Type", "text/css;charset=utf-8")
1113 _, err := c.Status(fiber.StatusOK).Write([]byte(h.StyleCSS()))
1114 return err
1115}
1116
1117func serveWASM(r *fiber.App) {
1118 if f.WasmExecPath != "" {
1119 _, err := script.File(f.WasmExecPath).Bytes()
1120 if err != nil {
1121 log.Printf("Error reading %s: %v\n", f.WasmExecPath, err)
1122 } else { //the wasm exec must be present or none of the webassembly stuff will work ; provided by the golang installaton
1123 r.Get(f.WasmExecPathTinyGo, func(c fiber.Ctx) error {
1124 wasmExecData, err := script.File(f.WasmExecPathTinyGo).Bytes()
1125 if err != nil {
1126 log.Printf("Error reading %s: %v\n", f.WasmExecPathTinyGo, err)
1127 return c.SendStatus(fiber.StatusNotFound)
1128 }
1129 c.Set("Content-Type", "application/js")
1130 _, err = c.Status(fiber.StatusOK).Write(wasmExecData)
1131 return err
1132 })
1133
1134 r.Get(f.WasmExecPathGo, func(c fiber.Ctx) error {
1135 wasmExecData, err := script.File(f.WasmExecPathGo).Bytes()
1136 if err != nil {
1137 log.Printf("Error reading %s: %v\n", f.WasmExecPathGo, err)
1138 return c.SendStatus(fiber.StatusNotFound)
1139 }
1140 c.Set("Content-Type", "application/js")
1141 _, err = c.Status(fiber.StatusOK).Write(wasmExecData)
1142 return err
1143 })
1144
1145 suffix := ".wasm"
1146 if f.UseTinygo {
1147 suffix = "-tiny.wasm"
1148 }
1149 for _, wasmSRC := range f.WasmSRC {
1150 outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + suffix
1151 r.Get("/"+outputFile, func(c fiber.Ctx) error {
1152 data, err := script.File(outputFile).Bytes()
1153 if err != nil {
1154 script.File(outputFile).Stdout() //nolint
1155 return c.SendStatus(fiber.StatusInternalServerError)
1156 }
1157 c.Set("Content-Type", "application/wasm")
1158 return c.Status(fiber.StatusOK).Send(data)
1159 })
1160 }
1161 }
1162 }
1163}
1164
1165func sendFile(c fiber.Ctx) error {
1166 return c.SendFile("." + c.Path())
1167}
1168func sendImage(c fiber.Ctx) error {
1169 c.Set("Content-Type", "image/jpeg")
1170 return c.SendFile("./img" + c.Path())
1171}
1172
1173func stlbase64(c fiber.Ctx) error {
1174 stlfile, err := script.File("img/stl/" + c.Params("filename")).Bytes()
1175 if err != nil {
1176 _, _ = script.File("img/stl/" + c.Params("filename")).Stdout() //nolint
1177 return c.SendStatus(fiber.StatusNotFound)
1178 }
1179 _, err = c.Status(fiber.StatusOK).Write([]byte("data:model/stl;base64," + base64.StdEncoding.EncodeToString(stlfile)))
1180 return err
1181}
1182
1183type item struct {
1184 ID string
1185 Amount int64
1186}
1187
1188func cathtmlfunc(c fiber.Ctx) error {
1189 tmpl, err := mainTmpl()
1190 if err != nil {
1191 msg := fmt.Sprintf("Error parsing html template: %v", err)
1192 log.Println(msg)
1193 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1194 }
1195 tmpl0, err := tmpl.Clone()
1196 if err != nil {
1197 msg := fmt.Sprintf("Error cloning html template: %v", err)
1198 log.Println(msg)
1199 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1200 }
1201 _, err = tmpl0.New("main").Parse(h.CategoryPage())
1202 if err != nil {
1203 msg := fmt.Sprintf("Error parsing Category page template: %v", err)
1204 log.Println(msg)
1205 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1206 }
1207 tmpl = tmpl0
1208 var tmplData map[string]interface{}
1209 var result bytes.Buffer
1210 var categoryproducts p.Products
1211 c.Set("Content-Type", "text/html;charset=utf-8")
1212 h1 := pageMeta(c, htmlPageTemplateData)
1213 h1.Title = fmt.Sprintf("%s | %s", func() string {
1214 var str string
1215 if c.Params("partno") != "" {
1216 return "No product matching partno.: " + c.Params("partno") + " | Showing All Products"
1217 }
1218 if c.Params("cat") == "" {
1219 return "All Products"
1220 }
1221 str = fmt.Sprintf("Category: %s", c.Params("cat"))
1222 if c.Params("subcat") != "" {
1223 str += fmt.Sprintf("; Subcategory: %s", c.Params("subcat"))
1224 }
1225 return str
1226 }(), h1.Title)
1227 h1.Page = "category"
1228 if c.Params("cat") == "" && c.Params("subcat") == "" {
1229 tmplData = map[string]interface{}{
1230 "Products": allproducts,
1231 "Page": h1,
1232 "Category": c.Params("cat"),
1233 "Subcategory": c.Params("subcat"),
1234 "Prods": allproducts,
1235 "Product": c.Params("partno"),
1236 }
1237 } else {
1238
1239 for _, prod := range allproducts {
1240 if strings.EqualFold(prod.Category, c.Params("cat")) && (c.Params("subcat") == "" || strings.EqualFold(escapesubcat(prod.Subcategory), c.Params("subcat"))) {
1241 categoryproducts = append(categoryproducts, prod)
1242 }
1243 }
1244 tmplData = map[string]interface{}{
1245 "Products": categoryproducts,
1246 "Page": h1,
1247 "Category": c.Params("cat"),
1248 "Subcategory": c.Params("subcat"),
1249 "Prods": allproducts,
1250 }
1251 }
1252 err = tmpl.Execute(&result, tmplData)
1253 if err != nil {
1254 msg := fmt.Sprintf("Error execute html template: %v", err)
1255 log.Println(msg)
1256 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1257 }
1258 _, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1259 return err
1260}
1261
1262func getcats() (cats []string) {
1263 var catsMap = make(map[string]int)
1264 for _, prod := range allproducts {
1265 catsMap[prod.Category]++
1266 }
1267 for cat := range catsMap {
1268 cats = append(cats, cat)
1269 }
1270 return cats
1271}
1272func contains(slice []string, str string) bool {
1273 for _, s := range slice {
1274 if s == str {
1275 return true
1276 }
1277 }
1278 return false
1279}
1280func getcategories(allproducts p.Products) (map[string]int, []string, map[string]map[string]int, map[string][]string) {
1281 categoryCounts := make(map[string]int)
1282 subcategoryCounts := make(map[string]map[string]int)
1283 subcategoriesByCategory := make(map[string][]string)
1284
1285 for _, prod := range allproducts {
1286 if prod.Category != "" {
1287 categoryCounts[prod.Category]++
1288 if prod.Subcategory != "" {
1289 if subcategoryCounts[prod.Category] == nil {
1290 subcategoryCounts[prod.Category] = make(map[string]int)
1291 }
1292 subcategoryCounts[prod.Category][prod.Subcategory]++
1293 if !contains(subcategoriesByCategory[prod.Category], prod.Subcategory) {
1294 subcategoriesByCategory[prod.Category] = append(subcategoriesByCategory[prod.Category], prod.Subcategory)
1295 }
1296 }
1297 }
1298 }
1299
1300 var sortableCategories []struct {
1301 Name string
1302 Count int
1303 }
1304 for cat, count := range categoryCounts {
1305 sortableCategories = append(sortableCategories, struct {
1306 Name string
1307 Count int
1308 }{Name: cat, Count: count})
1309 }
1310 sort.Slice(sortableCategories, func(i, j int) bool {
1311 return sortableCategories[i].Count > sortableCategories[j].Count
1312 })
1313 var sortedCategories []string
1314 for _, cat := range sortableCategories {
1315 sortedCategories = append(sortedCategories, cat.Name)
1316 var sortableSubcategories []struct {
1317 Name string
1318 Count int
1319 }
1320 for subcat, count := range subcategoryCounts[cat.Name] {
1321 sortableSubcategories = append(sortableSubcategories, struct {
1322 Name string
1323 Count int
1324 }{Name: subcat, Count: count})
1325 }
1326 sort.Slice(sortableSubcategories, func(i, j int) bool {
1327 return sortableSubcategories[i].Count > sortableSubcategories[j].Count
1328 })
1329 var sortedSubcategories []string
1330 for _, subcat := range sortableSubcategories {
1331 sortedSubcategories = append(sortedSubcategories, subcat.Name)
1332 }
1333 subcategoriesByCategory[cat.Name] = sortedSubcategories
1334 }
1335 return categoryCounts, sortedCategories, subcategoryCounts, subcategoriesByCategory
1336}
1337
1338func getsubcats(cat string) (subcats []string) {
1339 var subcatsMap = make(map[string]int)
1340 for _, prod := range allproducts {
1341 if cat == "" || strings.EqualFold(cat, prod.Category) {
1342 if prod.Subcategory != "" {
1343 subcatsMap[escapesubcat(prod.Subcategory)]++
1344 }
1345 }
1346 }
1347 for subcat := range subcatsMap {
1348 subcats = append(subcats, subcat)
1349 }
1350 return subcats
1351}
1352func escapesubcat(sc string) (esc string) {
1353 esc = strings.Replace(sc, "ΒΌ", "quarter-", -1)
1354 esc = strings.Replace(esc, "Β½", "half-", -1)
1355 esc = strings.Replace(esc, "1/16", "sixteenth-", -1)
1356 esc = strings.Replace(esc, "%", "-pct", -1)
1357 esc = strings.Replace(esc, " ", " ", -1)
1358 esc = strings.Replace(esc, " ", "-", -1)
1359 esc = strings.Replace(esc, "--", "-", -1)
1360 esc = strings.Replace(esc, "watt1", "watt-1", -1)
1361 esc = strings.Replace(esc, "watt5", "watt-5", -1)
1362 return esc
1363}
1364
1365func handlecat(c fiber.Ctx) error {
1366 if c.Params("cat") == "" && c.Params("subcat") == "" {
1367 return cathtmlfunc(c)
1368 }
1369 var catexists bool
1370 var subcatexists bool
1371 catexists = false
1372 for _, cat := range getcats() {
1373 if strings.EqualFold(cat, c.Params("cat")) {
1374 catexists = true
1375 break
1376 }
1377 }
1378 subcatexists = false
1379 if c.Params("subcat") != "" {
1380 for _, subcat := range getsubcats("") {
1381 if strings.EqualFold(escapesubcat(subcat), c.Params("subcat")) {
1382 subcatexists = true
1383 break
1384 }
1385 }
1386 }
1387 if c.Params("subcat") != "" && !subcatexists {
1388 log.Printf("subcategory %s does not match any existing subcategory\n", c.Params("subcat"))
1389 return c.Redirect().To("/cat/" + c.Params("cat"))
1390 }
1391 if !catexists {
1392 log.Printf("category %s does not match any existing category\n", c.Params("cat"))
1393 return c.Redirect().To("/cat")
1394 }
1395 if catexists || (catexists && subcatexists) {
1396 return cathtmlfunc(c)
1397 }
1398 return c.SendStatus(fiber.StatusNotFound)
1399}
1400
1401func homepage(c fiber.Ctx) error {
1402 tmpl, err := mainTmpl()
1403 if err != nil {
1404 msg := fmt.Sprintf("Could not parsing html template: %v", err)
1405 log.Println(msg)
1406 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1407 }
1408 tmpl0, err := tmpl.Clone()
1409 if err != nil {
1410 msg := fmt.Sprintf("Error cloning template: %v", err)
1411 log.Println(msg)
1412 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1413 }
1414 _, err = tmpl0.New("main").Parse(h.FrontPage())
1415 if err != nil {
1416 msg := fmt.Sprintf("Error parsing Front Page template: %v", err)
1417 log.Println(msg)
1418 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1419 }
1420 _, err = tmpl0.New("about").Parse(h.AboutPage())
1421 if err != nil {
1422 msg := fmt.Sprintf("Error parsing About Page template: %v", err)
1423 log.Println(msg)
1424 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1425 }
1426 _, err = tmpl0.New("policy").Parse(h.PolicyPage())
1427 if err != nil {
1428 msg := fmt.Sprintf("Error parsing Policy Page template: %v", err)
1429 log.Println(msg)
1430 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1431 }
1432 _, err = tmpl0.New("links").Parse(h.LinksPage())
1433 if err != nil {
1434 msg := fmt.Sprintf("Error parsing Links Page template: %v", err)
1435 log.Println(msg)
1436 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1437 }
1438 tmpl = tmpl0
1439 log.Println(c.Get("User-Agent"))
1440 c.Set("Content-Type", "text/html;charset=utf-8")
1441 h1 := pageMeta(c, htmlPageTemplateData)
1442 tmplData := map[string]interface{}{
1443 "Page": h1,
1444 "Prods": allproducts,
1445 }
1446 var result bytes.Buffer
1447 err = tmpl.Execute(&result, tmplData)
1448 if err != nil {
1449 msg := fmt.Sprintf("Error executing template: %v", err)
1450 log.Println(msg)
1451 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1452 }
1453 _, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1454 return err
1455}
1456
1457func productpage(c fiber.Ctx) error {
1458 tmpl, err := mainTmpl()
1459 if err != nil {
1460 msg := fmt.Sprintf("Error parsing html template: %v", err)
1461 log.Println(msg)
1462 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1463 }
1464 tmpl0, err := tmpl.Clone()
1465 if err != nil {
1466 msg := fmt.Sprintf("Error cloning template: %v", err)
1467 log.Println(msg)
1468 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1469 }
1470 _, err = tmpl0.New("main").Parse(h.ProductPage())
1471 if err != nil {
1472 msg := fmt.Sprintf("Error parsing product page template: %v", err)
1473 log.Println(msg)
1474 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1475 }
1476 tmpl = tmpl0
1477 c.Set("Content-Type", "text/html;charset=utf-8")
1478 for _, prod := range allproducts {
1479 if strings.EqualFold(prod.Partno, c.Params("partno")) {
1480 var result bytes.Buffer
1481 h1 := pageMeta(c, htmlPageTemplateData)
1482 h1.Page = "product"
1483 h1.Title = fmt.Sprintf("%s | %s", prod.Name, h1.Title)
1484 tmplData := map[string]interface{}{
1485 "Prod": prod,
1486 "Page": h1,
1487 "Prods": allproducts,
1488 }
1489 err := tmpl.Execute(&result, tmplData)
1490 if err != nil {
1491 log.Println("error: ", err)
1492 _, err = c.Status(fiber.StatusInternalServerError).Write(result.Bytes())
1493 return err
1494 }
1495 _, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1496 return err
1497 }
1498 }
1499 log.Printf("product %s does not match any existing product\n", c.Params("partno"))
1500 return c.Status(fiber.StatusNotFound).Redirect().To("/cat")
1501}
1502
1503
1504// ===== order.go =====
1505// Package main stripe.go
1506package main
1507
1508import (
1509 "bytes"
1510 "encoding/json"
1511 "fmt"
1512 htmpl "html/template"
1513 "log"
1514 "os"
1515 "path/filepath"
1516 "strings"
1517 "time"
1518
1519 "github.com/bitfield/script"
1520 "github.com/gofiber/fiber/v3"
1521 "github.com/stripe/stripe-go/v81"
1522 "github.com/stripe/stripe-go/v81/paymentintent"
1523)
1524
1525func handleOrder(r *fiber.App) {
1526 r.Get("/checkout.css", func(c fiber.Ctx) error {
1527 c.Set("Content-Type", "text/css;charset=utf-8")
1528 _, err := c.Status(fiber.StatusOK).Write([]byte(h.CheckoutCSS()))
1529 return err
1530 })
1531
1532 r.Get("/complete", func(c fiber.Ctx) error {
1533 // Complete template
1534 completetmpl := htmpl.New("index")
1535 if _, err := completetmpl.Parse(h.CompletePage()); err != nil {
1536 msg := fmt.Sprintf("Error parsing complete page template: %v", err)
1537 log.Println(msg)
1538 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1539 }
1540 if _, err := completetmpl.New("wasm").Parse(h.Wasm()); err != nil {
1541 log.Println("Error parsing wasm template:", err)
1542 msg := fmt.Sprintf("Error parsing wasm template: %v", err)
1543 log.Println(msg)
1544 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1545 }
1546 h1 := htmlPageTemplateData
1547 /*
1548 proto := "http"
1549 if c.Secure() {
1550 proto += "s"
1551 }
1552 */
1553 proto := "https"
1554 h1.Canonical = proto + `://` + c.Hostname() + c.OriginalURL()
1555 h1.BaseURL = proto + `://` + c.Hostname()
1556 h1.RequestHost = c.Hostname()
1557 h1.Protocol = proto
1558 h1.Time = time.Now().Format(time.RFC3339Nano)
1559 h1.Year = fmt.Sprintf("%v", time.Now().Year())
1560 tmplData := map[string]interface{}{
1561 "Page": h1,
1562 }
1563 var result bytes.Buffer
1564 err := completetmpl.Execute(&result, tmplData)
1565 if err != nil {
1566 msg := fmt.Sprintf("Could not execute html template %v", err)
1567 log.Println(msg)
1568 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1569 }
1570 c.Set("Content-Type", "text/html;charset=utf-8")
1571 return c.Status(fiber.StatusOK).Send(result.Bytes())
1572 })
1573
1574 r.Get("/order/:piid", func(c fiber.Ctx) error {
1575 piid := c.Params("piid")
1576 order, err := script.File("orders/" + piid + ".json").Bytes()
1577 if err != nil {
1578 return c.Status(fiber.StatusNotFound).SendString("Order not found")
1579 }
1580 return c.Status(fiber.StatusOK).Send(order)
1581 })
1582
1583 r.Get("/order/:piid/html", func(c fiber.Ctx) error {
1584 piid := c.Params("piid")
1585 order, err := script.File("orders/" + piid + ".json").Bytes()
1586 if err != nil {
1587 return c.Status(fiber.StatusNotFound).SendString("Order not found")
1588 }
1589 var m map[string]interface{}
1590 if err := json.Unmarshal(order, &m); err != nil { return c.Status(500).SendString("failed to unmarshal order json: "+err.Error()) }
1591 receipt, err := buildReceipt(m, piid)
1592 if err != nil { return c.Status(500).SendString("failed to build reciept: "+err.Error()) }
1593 return c.Status(200).SendString(string(receipt))
1594 })
1595
1596 r.Post("/create-payment-intent", func(c fiber.Ctx) error {
1597 rawBody := c.Body()
1598 if rawBody == nil {
1599 log.Printf("Failed to read raw request body")
1600 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to read request body"})
1601 }
1602 log.Printf("Raw request body: %s", string(rawBody))
1603
1604 var req struct {
1605 Items []item `json:"items"`
1606 }
1607 if err := json.Unmarshal(rawBody, &req); err != nil {
1608 log.Printf("Failed to parse JSON: %v", err)
1609 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
1610 }
1611
1612 total := int64(0)
1613 for _, item := range req.Items {
1614 total += item.Amount
1615 }
1616
1617 params := &stripe.PaymentIntentParams{
1618 Amount: stripe.Int64(total),
1619 Currency: stripe.String(string(stripe.CurrencyUSD)),
1620 }
1621 pi, err := paymentintent.New(params)
1622 if err != nil {
1623 log.Printf("Failed to create PaymentIntent: %v", err)
1624 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
1625 }
1626
1627 log.Printf("Created PaymentIntent with ClientSecret: %v", pi.ClientSecret)
1628 return c.Status(fiber.StatusOK).JSON(fiber.Map{
1629 "clientSecret": pi.ClientSecret,
1630 "dpmCheckerLink": fmt.Sprintf("https://dashboard.stripe.com/settings/payment_methods/review?transaction_id=%s", pi.ID),
1631 })
1632 })
1633
1634 r.Post("/submit-order", func(c fiber.Ctx) error {
1635 var requestData struct {
1636 LocalStorageData map[string]interface{} `json:"localStorageData"`
1637 PaymentIntentID string `json:"paymentIntentId"`
1638 }
1639
1640 if err := c.Bind().Body(&requestData); err != nil {
1641 log.Println(err)
1642 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request data"})
1643 }
1644
1645 log.Printf("Received order data: %+v\n", requestData.LocalStorageData)
1646 log.Printf("Received payment intent ID: %s\n", requestData.PaymentIntentID)
1647
1648 paymentIntent, err := paymentintent.Get(requestData.PaymentIntentID, nil)
1649 if err != nil {
1650 log.Printf("Error retrieving payment intent: %v", err)
1651 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to verify payment"})
1652 }
1653 if paymentIntent.Status != stripe.PaymentIntentStatusSucceeded {
1654 log.Printf("Payment was not successful, status: %s", paymentIntent.Status)
1655 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Payment not successful"})
1656 }
1657
1658 ordersDir := "./orders"
1659 if err := os.MkdirAll(ordersDir, os.ModePerm); err != nil {
1660 log.Printf("Error creating orders directory: %v", err)
1661 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1662 }
1663
1664 filePath := filepath.Join(ordersDir, fmt.Sprintf("%s.json", requestData.PaymentIntentID))
1665 data, err := json.MarshalIndent(requestData.LocalStorageData, "", " ")
1666 if err != nil {
1667 log.Printf("Error marshaling data to json: %v", err)
1668 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1669 }
1670 if err := os.WriteFile(filePath, data, 0o644); err != nil {
1671 log.Printf("Error writing data to file: %v", err)
1672 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1673 }
1674
1675 // ---- Print receipt via CUPS (non-blocking so your response is snappy)
1676 go func(pid string, local map[string]interface{}) {
1677 receipt, err := buildReceipt(local, pid)
1678 if err != nil {
1679 log.Printf("build receipt failed: %v", err)
1680 return
1681 }
1682 if err := sendToCUPS(receipt, "Order "+pid); err != nil {
1683 log.Printf("print failed: %v", err)
1684 // optional: mark a sentinel file so you can reprint later
1685 _ = os.WriteFile(filepath.Join(ordersDir, pid+".print_failed"), []byte(err.Error()), 0o644)
1686 }
1687 }(requestData.PaymentIntentID, requestData.LocalStorageData)
1688
1689 return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Order submitted successfully"})
1690 })
1691
1692 /*
1693 r.Post("/reprint/:pid", func(c fiber.Ctx) error {
1694 pid := c.Params("pid")
1695 b, err := os.ReadFile(filepath.Join("./orders", pid+".json"))
1696 if err != nil { return c.Status(404).SendString("not found") }
1697 var m map[string]interface{}
1698 if err := json.Unmarshal(b, &m); err != nil { return c.Status(500).SendString(err.Error()) }
1699 receipt, err := buildReceipt(m, pid)
1700 if err != nil { return c.Status(500).SendString(err.Error()) }
1701 if err := sendToCUPS(receipt, "Order "+pid); err != nil {
1702 return c.Status(500).SendString(err.Error())
1703 }
1704 return c.SendStatus(204)
1705 })
1706 */
1707}
1708
1709func buildReceipt(local map[string]interface{}, paymentIntentID string) ([]byte, error) {
1710 // Pretty JSON body from what you already persisted
1711 body, err := json.MarshalIndent(local, "", " ")
1712 if err != nil {
1713 return nil, err
1714 }
1715 // Simple text receipt header
1716 ts := time.Now().Format("2006-01-02 15:04:05")
1717 hdr := fmt.Sprintf(
1718 "==================== ORDER ====================\n"+
1719 "PaymentIntent: %s\nTime: %s\n===============================================\n\n",
1720 paymentIntentID, ts,
1721 )
1722 // Footer (optional)
1723 ftr := "\n\n---------------------- END ---------------------\n"
1724 receipt := append([]byte(hdr), body...)
1725 receipt = append(receipt, []byte(ftr)...)
1726 return receipt, nil
1727}
1728
1729// escape for inclusion inside *double quotes* in a bash command string
1730func bashEscapeDoubleQuoted(s string) string {
1731 s = strings.ReplaceAll(s, `\`, `\\`)
1732 s = strings.ReplaceAll(s, `"`, `\"`)
1733 s = strings.ReplaceAll(s, "$", `\$`)
1734 s = strings.ReplaceAll(s, "`", "\\`")
1735 return s
1736}
1737
1738func sendToCUPS(receipt []byte, title string) error {
1739 if title == "" {
1740 title = "Order"
1741 }
1742 var cmd strings.Builder
1743 cmd.WriteString("lp")
1744
1745 if f.PrinterName != "" {
1746 cmd.WriteString(` -d "`)
1747 cmd.WriteString(bashEscapeDoubleQuoted(f.PrinterName))
1748 cmd.WriteString(`"`)
1749 }
1750
1751 cmd.WriteString(` -t "`)
1752 cmd.WriteString(bashEscapeDoubleQuoted(title))
1753 cmd.WriteString(`"`)
1754
1755 if f.CupsOptions != "" {
1756 for _, opt := range strings.Split(f.CupsOptions, ",") {
1757 opt = strings.TrimSpace(opt)
1758 if opt == "" {
1759 continue
1760 }
1761 cmd.WriteString(` -o "`)
1762 cmd.WriteString(bashEscapeDoubleQuoted(opt))
1763 cmd.WriteString(`"`)
1764 }
1765 }
1766
1767 full := fmt.Sprintf(`bash -lc %q`, cmd.String())
1768
1769 _, err := script.Echo(string(receipt)).Exec(full).Stdout()
1770 if err != nil {
1771 return fmt.Errorf("lp failed: %v", err)
1772 }
1773 return nil
1774}
1775
1776
1777// ===== other.go =====
1778// Package main other.go
1779package main
1780
1781import (
1782 "bytes"
1783 "fmt"
1784 "log"
1785
1786 "github.com/gofiber/fiber/v3"
1787)
1788
1789func handleOthers(r *fiber.App) {
1790 r.Get("/COVID", func(c fiber.Ctx) error {
1791 tmpl, err := mainTmpl()
1792 if err != nil {
1793 msg := fmt.Sprintf("Error parse html template: %v", err)
1794 log.Println(msg)
1795 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1796 }
1797 tmpl0, err := tmpl.Clone()
1798 if err != nil {
1799 msg := fmt.Sprintf("Error cloning template: %v", err)
1800 log.Println(msg)
1801 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1802 }
1803 _, err = tmpl0.New("main").Parse(h.COVIDPage())
1804 if err != nil {
1805 msg := fmt.Sprintf("Error parsing main template: %v", err)
1806 log.Println(msg)
1807 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1808 }
1809 tmpl = tmpl0
1810 log.Println(c.Get("User-Agent"))
1811 c.Set("Content-Type", "text/html;charset=utf-8")
1812 h1 := pageMeta(c, htmlPageTemplateData)
1813 h1.Page = "hidden"
1814 h1.MetaDesc = "The COVID ΜΆvΜΆaΜΆcΜΆcΜΆiΜΆnΜΆeΜΆ bioweapon injection genocide and the new dark age of humanity"
1815 // h1.Mobile = strings.Contains(strings.ToLower(c.Get("User-Agent")), "mobile")
1816 tmplData := map[string]interface{}{
1817 "Page": h1,
1818 "Prods": allproducts,
1819 }
1820 var result bytes.Buffer
1821 err = tmpl.Execute(&result, tmplData)
1822 if err != nil {
1823 msg := fmt.Sprintf("Error executing template: %v", err)
1824 log.Println(msg)
1825 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1826 }
1827 _, err = c.Status(fiber.StatusOK).Write(bytes.Replace(bytes.Replace(bytes.Replace(bytes.Replace(bytes.Replace(bytes.Replace(bytes.Replace(result.Bytes(), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1))
1828 return err
1829 })
1830
1831}
1832
1833
1834// ===== source.go =====
1835// Package main source.go
1836package main
1837
1838import (
1839 "bytes"
1840 "embed"
1841 "fmt"
1842 "io/fs"
1843 "os"
1844 "strings"
1845
1846 "github.com/alecthomas/chroma/v2"
1847 "github.com/alecthomas/chroma/v2/formatters/html"
1848 "github.com/alecthomas/chroma/v2/lexers"
1849 "github.com/alecthomas/chroma/v2/styles"
1850 "github.com/gofiber/fiber/v3"
1851)
1852
1853//go:embed *.go
1854var quine embed.FS
1855
1856var sourceWasm = os.DirFS("wasm")
1857var sourcesWasm []fs.FS
1858var sourceCore = os.DirFS("ui")
1859var sourceHtml = os.DirFS("htmpl")
1860var sourceContent = os.DirFS("content")
1861
1862func serveSourceCode(r *fiber.App) {
1863 for _, wasmSRC := range f.WasmSRC {
1864 sourcesWasm = append(sourcesWasm, os.DirFS(wasmSRC))
1865 }
1866 r.Get("/sourcecode", func(c fiber.Ctx) error {
1867 ret := `<!doctype html>
1868<html lang='en'>
1869<head>
1870<link rel="stylesheet" href="/style.css" type="text/css">
1871</head>
1872<body class='grid-container' style='background-color:black;color:white;'>
1873<a href='/sourcecode/go'>GO</a><br><br>
1874
1875<a href='/sourcecode/html'>HTML</a><br><br>
1876
1877<a href='/sourcecode/content'>Content</a><br><br>
1878
1879<a href='/sourcecode/core'>C.O.R.E.</a><br><br>
1880
1881<a href='/sourcecode/wasm'>WASM</a><br><br>
1882
1883</body>
1884</html>
1885`
1886 c.Set("Content-Type", "text/html;charset=utf-8")
1887 _, err := c.Status(fiber.StatusOK).Write([]byte(ret))
1888 return err
1889 })
1890
1891 r.Get("/sourcecode/html", sourcecodehtml)
1892 r.Get("/sourcecode/content", sourcecodecontent)
1893 r.Get("/sourcecode/go", sourcecodego)
1894 r.Get("/sourcecode/core", sourcecodecore)
1895 // r.Get("/sourcecodewasm", sourcecodewasm)
1896 r.Get("/sourcecode/wasm", func(c fiber.Ctx) error {
1897 ret := `<!doctype html>
1898<html lang='en'>
1899<head>
1900<link rel="stylesheet" href="/style.css" type="text/css">
1901</head>
1902<body class='grid-container' style='background-color:black;color:white;'>
1903`
1904 for _, wasmSRC := range f.WasmSRC {
1905 pathNameSlc := strings.Split(wasmSRC, "/")
1906 pathName := pathNameSlc[len(pathNameSlc)-1]
1907 ret += `<a href='/sourcecode/wasm/` + pathName + `'>` + pathName + `</a><br>
1908 `
1909 }
1910 ret += `</body></html>
1911 `
1912 c.Set("Content-Type", "text/html;charset=utf-8")
1913 _, err := c.Status(fiber.StatusOK).Write([]byte(ret))
1914 return err
1915 })
1916
1917 for i, wasmSRC := range f.WasmSRC {
1918 pathNameSlc := strings.Split(wasmSRC, "/")
1919 pathName := pathNameSlc[len(pathNameSlc)-1]
1920 r.Get("/sourcecode/wasm/"+pathName, func(c fiber.Ctx) error {
1921 return sourcecode(c, sourcesWasm[i], "dracula", "go")
1922 })
1923 }
1924}
1925
1926func sourcecodehtml(c fiber.Ctx) error {
1927 return sourcecode(c, sourceHtml, "monokai", "html")
1928}
1929func sourcecodecontent(c fiber.Ctx) error {
1930 return sourcecode(c, sourceContent, "monokai", "html")
1931}
1932func sourcecodego(c fiber.Ctx) error {
1933 return sourcecode(c, quine, "monokai", "go")
1934}
1935
1936func sourcecodewasm(c fiber.Ctx) error {
1937 return sourcecode(c, sourceWasm, "dracula", "go")
1938}
1939
1940func sourcecodecore(c fiber.Ctx) error {
1941 return sourcecode(c, sourceCore, "solarized-dark256", "go")
1942}
1943
1944func sourcecode(c fiber.Ctx, fsys fs.FS, styleName string, lang string) error {
1945 c.Set("Content-Type", "text/html;charset=utf-8")
1946 var buf bytes.Buffer
1947 var builder strings.Builder
1948
1949 fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
1950 if err != nil {
1951 return err
1952 }
1953 if !d.IsDir() && strings.HasSuffix(path, "."+lang) {
1954 content, err := fs.ReadFile(fsys, path)
1955 if err != nil {
1956 return err
1957 }
1958 builder.WriteString(fmt.Sprintf("// ===== %s =====\n", path))
1959 builder.Write(content)
1960 builder.WriteString("\n\n")
1961 }
1962 return nil
1963 })
1964
1965 // Pick lexer & style
1966 lexer := lexers.Get(lang)
1967 if lexer == nil {
1968 lexer = lexers.Fallback
1969 }
1970 lexer = chroma.Coalesce(lexer)
1971
1972 style := styles.Get(styleName)
1973 if style == nil {
1974 style = styles.Fallback
1975 }
1976
1977 // Formatter with line numbers & CSS classes
1978 formatter := html.New(
1979 html.WithLineNumbers(true),
1980 html.WithClasses(true),
1981 )
1982
1983 iterator, err := lexer.Tokenise(nil, builder.String())
1984 if err != nil {
1985 return err
1986 }
1987
1988 // Optional: include CSS in output
1989 var css bytes.Buffer
1990 _ = formatter.WriteCSS(&css, style)
1991 buf.WriteString("<style>")
1992 buf.Write(css.Bytes())
1993 buf.WriteString("</style>")
1994
1995 if err := formatter.Format(&buf, style, iterator); err != nil {
1996 return err
1997 }
1998
1999 _, err = c.Status(fiber.StatusOK).Write(buf.Bytes())
2000 return err
2001}
2002
2003
2004// ===== tmpl.go =====
2005// Package main tmpl.go
2006package main
2007
2008import (
2009 "bytes"
2010 "fmt"
2011 htmpl "html/template"
2012 "log"
2013 "os"
2014 "path/filepath"
2015 "sort"
2016 "strconv"
2017 "strings"
2018 ttmpl "text/template"
2019 "time"
2020
2021 p "github.com/0magnet/m2/pkg/product"
2022 "github.com/gofiber/fiber/v3"
2023)
2024
2025/*
2026//go:embed htmpl/*
2027var templatesFS embed.FS
2028
2029//go:embed content/*
2030var contentFS embed.FS
2031*/
2032/*
2033var (
2034 templatesFS = os.DirFS("htmpl")
2035 contentFS = os.DirFS("content")
2036)
2037*/
2038/*
2039func mustReadEmbeddedFileToString(path string, fs embed.FS) string {
2040 return string(mustReadEmbeddedFileToBytes(path, fs))
2041}
2042
2043func mustReadEmbeddedFileToBytes(path string, fs embed.FS) []byte {
2044 data, err := fs.ReadFile(path)
2045 if err != nil {
2046 panic(err)
2047 }
2048 return data
2049}
2050*/
2051
2052func mustReadFileToString(path string) string {
2053 return string(mustReadFileToBytes(path))
2054}
2055
2056func mustReadFileToBytes(path string) []byte {
2057 data, err := os.ReadFile(path) //nolint
2058 if err != nil {
2059 panic(err)
2060 }
2061 return data
2062}
2063
2064type htmlTemplate struct {
2065 Empty func() string
2066 Head func() string
2067 Logo func() string
2068 Header func() string
2069 Categories func() string
2070 CatSubcats func() string
2071 Footer func() string
2072 MainPage func() string
2073 AuxPage func() string
2074 FrontPage func() string
2075 CategoryPage func() string
2076 CategoryPageMD func() string
2077 ProductPage func() string
2078 ProductPageMD func() string
2079 Schema func() string
2080 Cart func() string
2081 XMLSitemap func() string
2082 Wasm func() string
2083 Clock func() string
2084 AboutPage func() string
2085 PolicyPage func() string
2086 LinksPage func() string
2087 CheckoutPage func() string
2088 CompletePage func() string
2089 CheckoutCSS func() string
2090 StyleCSS func() string
2091 COVIDPage func() string
2092}
2093
2094var h = htmlTemplate{
2095 Empty: func() string { return mustReadFileToString("htmpl/empty.html") },
2096 Head: func() string { return mustReadFileToString("htmpl/head.html") },
2097 Logo: func() string { return mustReadFileToString("htmpl/logo.html") },
2098 Header: func() string { return mustReadFileToString("htmpl/header.html") },
2099 Categories: func() string { return mustReadFileToString("htmpl/categories.html") },
2100 CatSubcats: func() string { return mustReadFileToString("htmpl/catsubcats.html") },
2101 Footer: func() string { return mustReadFileToString("htmpl/footer.html") },
2102 MainPage: func() string { return mustReadFileToString("htmpl/main.html") },
2103 AuxPage: func() string { return mustReadFileToString("htmpl/aux.html") },
2104 FrontPage: func() string { return mustReadFileToString("htmpl/front.html") },
2105 CategoryPage: func() string { return mustReadFileToString("htmpl/category.html") },
2106 CategoryPageMD: func() string { return mustReadFileToString("htmpl/category.md") },
2107 ProductPage: func() string { return mustReadFileToString("htmpl/product.html") },
2108 ProductPageMD: func() string { return mustReadFileToString("htmpl/product.md") },
2109 Schema: func() string { return mustReadFileToString("htmpl/schema.html") },
2110 Cart: func() string { return mustReadFileToString("htmpl/cart.html") },
2111 XMLSitemap: func() string { return mustReadFileToString("htmpl/sitemap.xml") },
2112 Wasm: func() string { return mustReadFileToString("htmpl/wasm.html") },
2113 CompletePage: func() string { return mustReadFileToString("htmpl/complete.html") },
2114 Clock: func() string { return mustReadFileToString("content/clock.html") },
2115 AboutPage: func() string { return mustReadFileToString("content/about.html") },
2116 PolicyPage: func() string { return mustReadFileToString("content/policy.html") },
2117 LinksPage: func() string { return mustReadFileToString("content/links.html") },
2118 CheckoutPage: func() string { return mustReadFileToString("content/checkout.html") },
2119 CheckoutCSS: func() string { return mustReadFileToString("content/checkout.css") },
2120 StyleCSS: func() string { return mustReadFileToString("content/style.css") },
2121 COVIDPage: func() string { return mustReadFileToString("content/mementomori.html") },
2122}
2123
2124var htmlPageTemplateData htmlTemplateData
2125
2126var funcs = htmpl.FuncMap{
2127 "replace": replace, "mul": mul, "div": div, "safeHTML": safeHTML,
2128 "safeJS": safeJS, "stripProtocol": stripProtocol, "add": add, "sub": sub,
2129 "toFloat": toFloat, "equalsIgnoreCase": equalsIgnoreCase,
2130 "getsubcats": getsubcats, "escapesubcat": escapesubcat,
2131 "sortsubcats": sortsubcats, "repeat": repeat, "subcatlink": subcatlink,
2132}
2133
2134func mainTmpl() (tmpl *htmpl.Template, err error) {
2135 tmpl = htmpl.New("index").Funcs(funcs)
2136 if _, err := tmpl.Parse(h.MainPage()); err != nil {
2137 log.Println("Error parsing index template:", err)
2138 return tmpl, err
2139 }
2140
2141 partials := []struct {
2142 Name string
2143 Content string
2144 }{
2145 {"head", h.Head()},
2146 {"schema", h.Schema()},
2147 {"header", h.Header()},
2148 {"catsubcats", h.CatSubcats()},
2149 {"categories", h.Categories()},
2150 {"footer", h.Footer()},
2151 {"cart", h.Cart()},
2152 {"wasm", h.Wasm()},
2153 }
2154
2155 for _, p := range partials {
2156 if _, err := tmpl.New(p.Name).Parse(p.Content); err != nil {
2157 log.Printf("Error parsing %s template: %v", p.Name, err)
2158 return tmpl, err
2159 }
2160 }
2161 return tmpl, err
2162}
2163
2164func auxTmpl() (tmpl *htmpl.Template, err error) {
2165 tmpl = htmpl.New("index").Funcs(funcs)
2166 if _, err := tmpl.Parse(h.AuxPage()); err != nil {
2167 log.Println("Error parsing index template:", err)
2168 return tmpl, err
2169 }
2170
2171 partials := []struct {
2172 Name string
2173 Content string
2174 }{
2175 {"head", h.Head()},
2176 {"schema", h.Empty()},
2177 {"wasm", h.Empty()},
2178 }
2179
2180 for _, p := range partials {
2181 if _, err := tmpl.New(p.Name).Parse(p.Content); err != nil {
2182 log.Printf("Error parsing %s template: %v", p.Name, err)
2183 return tmpl, err
2184 }
2185 }
2186 return tmpl, err
2187}
2188
2189func pageMeta(c fiber.Ctx, base htmlTemplateData) htmlTemplateData {
2190 h := base
2191 host := string(c.Request().Host())
2192 /*
2193 proto := "http"
2194 if c.Secure() {
2195 proto += "s"
2196 }
2197 */
2198 proto := "https"
2199 h.Canonical = proto + "://" + host + c.OriginalURL()
2200 h.BaseURL = proto + "://" + host
2201 h.RequestHost = host
2202 h.Protocol = proto
2203 h.CatsCounts, h.Cats, h.SubCatsCounts, h.SubCatsByCat = getcategories(allproducts)
2204 h.LenAllProducts = len(allproducts)
2205 h.Time = time.Now().Format(time.RFC3339Nano)
2206 h.Year = fmt.Sprintf("%v", time.Now().Year())
2207 h.MetaDesc = f.Sitemeta
2208 h.KeyWords = strings.Replace(f.Sitelongname, " ", ", ", -1)
2209 return h
2210}
2211
2212func initTMPL() {
2213 htmlPageTemplateData = htmlTemplateData{
2214 TestMode: f.Teststripekey,
2215 Title: f.Sitelongname,
2216 StripePK: f.StripePK,
2217 SiteName: f.Sitedomain,
2218 SiteTagLine: f.Sitetagline,
2219 SiteName1: htmpl.HTML(checkerBoard(f.Sitedomain)), //nolint
2220 SiteLongName: f.Sitelongname,
2221 SiteASCIILogo: htmpl.HTML(f.SiteASCIILogo), //nolint
2222 SitePrettyName: f.Siteprettyname,
2223 SitePrettyNameCap: f.Siteprettynamecap,
2224 SitePrettyNameCaps: f.Siteprettynamecaps,
2225 TelegramContact: f.Tgcontact,
2226 TelegramChannel: f.Tgchannel,
2227 WasmExecPath: f.WasmExecPath,
2228 WasmExecRel: f.WasmExecPath,
2229 Cats: getcats(),
2230 LenAllProducts: len(allproducts),
2231 ImgSRC: func() (ret string) {
2232 ret = f.Siteimagesrc
2233 if ret == "" {
2234 ret = "/i"
2235 }
2236 return ret
2237 }(),
2238 Page: "front",
2239 Time: time.Now().Format(time.RFC3339Nano),
2240 Year: fmt.Sprintf("%v", time.Now().Year()),
2241 }
2242 htmlPageTemplateData.CatsCounts, htmlPageTemplateData.Cats, htmlPageTemplateData.SubCatsCounts, htmlPageTemplateData.SubCatsByCat = getcategories(allproducts)
2243 htmlPageTemplateData.WasmBinary = wasmBinary()
2244
2245}
2246
2247func wasmBinary() (ret []string) {
2248 if len(f.WasmSRC) == 0 {
2249 return ret
2250 }
2251 if f.UseTinygo {
2252 for _, wasmSRC := range f.WasmSRC {
2253 outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + "-tiny.wasm"
2254 ret = append(ret, outputFile)
2255 }
2256 return ret
2257 }
2258 for _, wasmSRC := range f.WasmSRC {
2259 outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + ".wasm"
2260 ret = append(ret, outputFile)
2261 }
2262 return ret
2263}
2264
2265type xmlTemplateData struct {
2266 Cats []string
2267 SubCatsByCat map[string][]string
2268 Products p.Products
2269 Update string
2270}
2271
2272func generateSitemapXML() string {
2273 xmlSitemapTemplateData := xmlTemplateData{
2274 Products: allproducts,
2275 Update: time.Now().Format("2006-01-02"),
2276 }
2277 _, xmlSitemapTemplateData.Cats, _, xmlSitemapTemplateData.SubCatsByCat = getcategories(allproducts)
2278 var err1 error
2279 xtmpl, err1 := ttmpl.New("index").Funcs(ttmpl.FuncMap{"getsubcats": getsubcats}).Parse(h.XMLSitemap())
2280 if err1 != nil {
2281 log.Println("Error parsing index template:", err1)
2282 }
2283 var result bytes.Buffer
2284 err1 = xtmpl.Execute(&result, xmlSitemapTemplateData)
2285 if err1 != nil {
2286 log.Println("error: ", err1)
2287 }
2288 return result.String()
2289}
2290
2291func toFloat(s string) float64 {
2292 if s == "" {
2293 return 0.0
2294 }
2295 f, err := strconv.ParseFloat(s, 64)
2296 if err != nil {
2297 return 0.0
2298 }
2299 return f
2300}
2301
2302func checkerBoard(input string) string {
2303 var result strings.Builder
2304 for i, char := range input {
2305 // Wrap every other letter with the specified HTML
2306 if i%2 == 0 {
2307 result.WriteString(fmt.Sprintf("<span class='nv'>%c</span>", char))
2308 } else {
2309 result.WriteRune(char)
2310 }
2311 }
2312 return result.String()
2313}
2314
2315type htmlTemplateData struct {
2316 Title string
2317 MetaDesc string
2318 Canonical string
2319 BaseURL string
2320 ImgSRC string // url where images are hosted
2321 OrdersURL string // url where checkout is served from
2322 SiteName string
2323 SiteTagLine string
2324 SiteName1 htmpl.HTML //checkerboard - alternate swap text & bg color
2325 SiteLongName string
2326 SitePrettyName string //ππππππ₯π π€π‘πππ£π.πππ₯
2327 SitePrettyNameCap string //ππππππ₯π π€π‘πππ£π.πππ₯
2328 SitePrettyNameCaps string //ππΈπΎβπΌπππββπΌβπΌ.βπΌπ
2329 SiteASCIILogo htmpl.HTML
2330 TelegramContact string
2331 TelegramChannel string
2332 Protocol string
2333 RequestHost string
2334 KeyWords string
2335 Style htmpl.HTML
2336 Heading htmpl.HTML
2337 StripePK string
2338 Cats []string
2339 CatsCounts map[string]int
2340 SubCatsCounts map[string]map[string]int
2341 SubCatsByCat map[string][]string
2342 LenAllProducts int
2343 Mobile bool
2344 Gocanvas htmpl.HTML
2345 WasmBinary []string
2346 WasmExecPath string
2347 WasmExecRel string
2348 StyleFontFace htmpl.CSS
2349 Message htmpl.HTML
2350 Page string
2351 Year string
2352 Time string
2353 AboutHTML htmpl.HTML
2354 LinksHTML htmpl.HTML
2355 PolicyHTML htmpl.HTML
2356 TestMode bool
2357}
2358
2359func equalsIgnoreCase(a, b string) bool {
2360 return strings.EqualFold(strings.Join(strings.Fields(a), ""), strings.Join(strings.Fields(b), ""))
2361}
2362
2363func replace(s, o, n string) string {
2364 return strings.ReplaceAll(s, o, n)
2365}
2366func mul(a, b float64) float64 {
2367 return a * b
2368}
2369func div(a, b float64) float64 {
2370 return a / b
2371}
2372func add(a, b int) int {
2373 return a + b
2374}
2375func sub(a, b int) int {
2376 return a - b
2377}
2378func safeHTML(s string) htmpl.HTML {
2379 return htmpl.HTML(s) //nolint
2380}
2381func safeJS(s string) htmpl.JS {
2382 return htmpl.JS(s) //nolint
2383}
2384func stripProtocol(s string) string {
2385 return strings.Replace(strings.Replace(s, "https://", "", -1), "http://", "", -1)
2386}
2387func repeat(s string, count int) string {
2388 var result string
2389 for i := 0; i < count; i++ {
2390 result += s
2391 }
2392 return result
2393}
2394func sortsubcats(subcats []string, counts map[string]map[string]int) []string {
2395 sort.Slice(subcats, func(i, j int) bool {
2396 catI, catJ := subcats[i], subcats[j]
2397 countI, countJ := counts[catI]["count"], counts[catJ]["count"]
2398 return countI > countJ
2399 })
2400 return subcats
2401}
2402
2403func subcatlink(subcategory string) string {
2404 s := subcategory
2405 s = strings.ReplaceAll(s, "ΒΌ", "quarter-")
2406 s = strings.ReplaceAll(s, "Β½", "half-")
2407 s = strings.ReplaceAll(s, "1/16", "sixteenth-")
2408 s = strings.ReplaceAll(s, "%", "-pct")
2409 s = strings.ReplaceAll(s, " ", " ")
2410 s = strings.ReplaceAll(s, "watt1", "watt-1")
2411 s = strings.ReplaceAll(s, "watt5", "watt-5")
2412 s = strings.ReplaceAll(s, " ", "-")
2413 s = strings.ReplaceAll(s, "--", "-")
2414 return s
2415}
2416
2417
2418// ===== wasm.go =====
2419// Package main wasm.go
2420package main
2421
2422import (
2423 "fmt"
2424 "log"
2425 "path/filepath"
2426 "strings"
2427 "time"
2428
2429 "github.com/bitfield/script"
2430 "github.com/briandowns/spinner"
2431)
2432
2433func compileWASM() {
2434 s := spinner.New(spinner.CharSets[14], 25*time.Millisecond)
2435 s.Suffix = " Compiling wasm..."
2436 for _, wasmSRC := range f.WasmSRC {
2437 ascend := strings.Repeat("../", len(strings.Split(wasmSRC, "/")))
2438 outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + ".wasm"
2439 compilecmd := fmt.Sprintf("bash -c 'cd %s || exit 1 ; time GOOS=js GOARCH=wasm %s -o %s %s -ldflags=\"-s -w\" %s && cd %s && du %s'", wasmSRC, f.Gobuild, ascend+outputFile, ldflags(wasmSRC), ".", ascend, outputFile)
2440 log.Println("Compiling wasm with:")
2441 log.Println(compilecmd)
2442 s.Start()
2443 _, err := script.Exec(compilecmd).Stdout()
2444 if err != nil {
2445 log.Fatal(err)
2446 }
2447 s.Stop()
2448 log.Println("Compiled wasm!")
2449 }
2450 for _, wasmSRC := range f.WasmSRC {
2451 ascend := strings.Repeat("../", len(strings.Split(wasmSRC, "/")))
2452 outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + "-tiny.wasm"
2453 compilecmd := fmt.Sprintf("bash -c 'cd %s || exit 1 ; time GOOS=js GOARCH=wasm %s -o %s %s %s && cd %s && du %s'", wasmSRC, f.Tinygobuild, ascend+outputFile, ldflags(wasmSRC), ".", ascend, outputFile)
2454 log.Println("compiling wasm with:")
2455 log.Println(compilecmd)
2456 s.Start()
2457 _, err := script.Exec(compilecmd).Stdout()
2458 if err != nil {
2459 log.Fatal(err)
2460 }
2461 s.Stop()
2462 log.Println("Compiled wasm!")
2463 }
2464}
2465
2466func ldflags(s string) (ss string) {
2467 checkFiles, err := script.FindFiles(s).Slice()
2468 if err != nil {
2469 log.Fatal(err)
2470 }
2471 if f.LDFlagsX != "" {
2472 for _, s := range checkFiles {
2473 res, err := script.File(s).Match(strings.Split(f.LDFlagsX, "=")[0]).String()
2474 if err != nil {
2475 log.Fatal(err)
2476 }
2477 if res != "" {
2478 ss += fmt.Sprintf(` -X 'main.%s' `, f.LDFlagsX)
2479 break
2480 }
2481 }
2482 }
2483 for _, s := range checkFiles {
2484 res, err := script.File(s).Match("wasmName").String()
2485 if err != nil {
2486 log.Fatal(err)
2487 }
2488 if res != "" {
2489 ss += fmt.Sprintf(` -X 'main.wasmName=%s' `, strings.TrimSuffix(filepath.Base(s), filepath.Ext(s))+".wasm")
2490 break
2491 }
2492 }
2493 if ss != "" {
2494 ss = `-ldflags="` + ss + `"`
2495 }
2496 return ss
2497}
2498
2499