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