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