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("/p/:partno", productpage)
1090 r.Get("/post/:partno", handlecat)
1091 r.Get("/p", handlecat)
1092 r.Get("/cat", handlecat)
1093 r.Get("/cat/:cat", handlecat)
1094 r.Get("/cat/:cat/:subcat", handlecat)
1095 r.Get("/style.css", style)
1096 handleOthers(r)
1097 handleOrder(r)
1098 if !f.NoCore {
1099 handleCORE(r)
1100 }
1101 go func() {
1102 err := r.Listen(fmt.Sprintf(":%d", f.WebPort))
1103 if err != nil {
1104 log.Println("Error serving http: ", err)
1105 }
1106 wg.Done()
1107 }()
1108 if !f.NoCore {
1109 watchCORE()
1110 }
1111 compileWASM()
1112 wg.Wait()
1113}
1114
1115func sitemap(c fiber.Ctx) error {
1116 c.Type("xml", "utf-8")
1117 return c.SendString(generateSitemapXML())
1118}
1119
1120func clock(c fiber.Ctx) error {
1121 c.Set("Content-Type", "text/html;charset=utf-8")
1122 _, err := c.Status(fiber.StatusOK).Write([]byte(h.Clock()))
1123 return err
1124}
1125
1126func logo(c fiber.Ctx) error {
1127 tmpl, err := auxTmpl()
1128 if err != nil {
1129 msg := fmt.Sprintf("Error parsing html template: %v", err)
1130 log.Println(msg)
1131 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1132 }
1133 tmpl0, err := tmpl.Clone()
1134 if err != nil {
1135 msg := fmt.Sprintf("Error cloning template: %v", err)
1136 log.Println(msg)
1137 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1138 }
1139 _, err = tmpl0.New("main").Parse(h.Logo())
1140 if err != nil {
1141 msg := fmt.Sprintf("Error parsing product page template: %v", err)
1142 log.Println(msg)
1143 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1144 }
1145 tmpl = tmpl0
1146 c.Set("Content-Type", "text/html;charset=utf-8")
1147
1148 img2txtFlags := ""
1149 if w, err := strconv.Atoi(c.Params("width")); err == nil {
1150 img2txtFlags = fmt.Sprintf("--width=%d ",w)
1151 }
1152 if h, err := strconv.Atoi(c.Params("height")); err == nil {
1153 img2txtFlags = fmt.Sprintf("--height=%d ",h)
1154 }
1155
1156 logoHTMLslice, err := script.Exec(fmt.Sprintf("bash -c 'img2txt %s logo.jpg | ansifilter -H'", img2txtFlags)).Slice()
1157 if err != nil {
1158 log.Println("error: ", err)
1159 _, err = c.Status(fiber.StatusInternalServerError).Write([]byte(err.Error()+"/n"+strings.Join(logoHTMLslice,"\n")))
1160 return err
1161 }
1162 if len(logoHTMLslice) > 2 {
1163 logoHTMLslice = logoHTMLslice[:len(logoHTMLslice)-3]
1164 }
1165 if len(logoHTMLslice) > 18 {
1166 logoHTMLslice = logoHTMLslice[19:]
1167 }
1168
1169
1170 var result bytes.Buffer
1171 h1 := pageMeta(c, htmlTemplateData{})
1172 h1.Page = "logo"
1173 h1.Title = "logo"
1174 tmplData := map[string]interface{}{
1175 "Content": strings.Join(logoHTMLslice, "\n"),
1176 }
1177 err = tmpl.Execute(&result, tmplData)
1178 if err != nil {
1179 log.Println("error: ", err)
1180 _, err = c.Status(fiber.StatusInternalServerError).Write(result.Bytes())
1181 return err
1182 }
1183 _, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1184 return err
1185}
1186
1187func robots(c fiber.Ctx) error {
1188 c.Set("Content-Type", "text/plain;charset=utf-8")
1189 _, err := c.Status(fiber.StatusOK).Write([]byte(fmt.Sprintf("User-agent: *\n\nSitemap: https://%s/sitemap.xml", c.Hostname())))
1190 return err
1191}
1192
1193func style(c fiber.Ctx) error {
1194 c.Set("Content-Type", "text/css;charset=utf-8")
1195 _, err := c.Status(fiber.StatusOK).Write([]byte(h.StyleCSS()))
1196 return err
1197}
1198
1199func serveWASM(r *fiber.App) {
1200 if f.WasmExecPath != "" {
1201 _, err := script.File(f.WasmExecPath).Bytes()
1202 if err != nil {
1203 log.Printf("Error reading %s: %v\n", f.WasmExecPath, err)
1204 } else { //the wasm exec must be present or none of the webassembly stuff will work ; provided by the golang installaton
1205 r.Get(f.WasmExecPathTinyGo, func(c fiber.Ctx) error {
1206 wasmExecData, err := script.File(f.WasmExecPathTinyGo).Bytes()
1207 if err != nil {
1208 log.Printf("Error reading %s: %v\n", f.WasmExecPathTinyGo, err)
1209 return c.SendStatus(fiber.StatusNotFound)
1210 }
1211 c.Set("Content-Type", "application/js")
1212 _, err = c.Status(fiber.StatusOK).Write(wasmExecData)
1213 return err
1214 })
1215
1216 r.Get(f.WasmExecPathGo, func(c fiber.Ctx) error {
1217 wasmExecData, err := script.File(f.WasmExecPathGo).Bytes()
1218 if err != nil {
1219 log.Printf("Error reading %s: %v\n", f.WasmExecPathGo, err)
1220 return c.SendStatus(fiber.StatusNotFound)
1221 }
1222 c.Set("Content-Type", "application/js")
1223 _, err = c.Status(fiber.StatusOK).Write(wasmExecData)
1224 return err
1225 })
1226
1227 suffix := ".wasm"
1228 if f.UseTinygo {
1229 suffix = "-tiny.wasm"
1230 }
1231 for _, wasmSRC := range f.WasmSRC {
1232 outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + suffix
1233 r.Get("/"+outputFile, func(c fiber.Ctx) error {
1234 data, err := script.File(outputFile).Bytes()
1235 if err != nil {
1236 script.File(outputFile).Stdout() //nolint
1237 return c.SendStatus(fiber.StatusInternalServerError)
1238 }
1239 c.Set("Content-Type", "application/wasm")
1240 return c.Status(fiber.StatusOK).Send(data)
1241 })
1242 }
1243 }
1244 }
1245}
1246
1247func sendFile(c fiber.Ctx) error {
1248 return c.SendFile("." + c.Path())
1249}
1250func sendImage(c fiber.Ctx) error {
1251 c.Set("Content-Type", "image/jpeg")
1252 return c.SendFile("./img" + c.Path())
1253}
1254
1255func stlbase64(c fiber.Ctx) error {
1256 name := c.Params("filename")
1257 if strings.ContainsAny(name, "/\\..") || strings.Contains(name, "..") {
1258 return c.SendStatus(fiber.StatusBadRequest)
1259 }
1260 stlfile, err := script.File("img/stl/" + name).Bytes()
1261 if err != nil {
1262 return c.SendStatus(fiber.StatusNotFound)
1263 }
1264 _, err = c.Status(fiber.StatusOK).Write([]byte("data:model/stl;base64," + base64.StdEncoding.EncodeToString(stlfile)))
1265 return err
1266}
1267
1268type item struct {
1269 ID string
1270 Amount int64
1271}
1272
1273func cathtmlfunc(c fiber.Ctx) error {
1274 tmpl, err := mainTmpl()
1275 if err != nil {
1276 msg := fmt.Sprintf("Error parsing html template: %v", err)
1277 log.Println(msg)
1278 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1279 }
1280 tmpl0, err := tmpl.Clone()
1281 if err != nil {
1282 msg := fmt.Sprintf("Error cloning html template: %v", err)
1283 log.Println(msg)
1284 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1285 }
1286 _, err = tmpl0.New("main").Parse(h.CategoryPage())
1287 if err != nil {
1288 msg := fmt.Sprintf("Error parsing Category page template: %v", err)
1289 log.Println(msg)
1290 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1291 }
1292 tmpl = tmpl0
1293 var tmplData map[string]interface{}
1294 var result bytes.Buffer
1295 var categoryproducts p.Products
1296 c.Set("Content-Type", "text/html;charset=utf-8")
1297 h1 := pageMeta(c, htmlPageTemplateData)
1298 h1.Title = fmt.Sprintf("%s | %s", func() string {
1299 var str string
1300 if c.Params("partno") != "" {
1301 return "No product matching partno.: " + c.Params("partno") + " | Showing All Products"
1302 }
1303 if c.Params("cat") == "" {
1304 return "All Products"
1305 }
1306 str = fmt.Sprintf("Category: %s", c.Params("cat"))
1307 if c.Params("subcat") != "" {
1308 str += fmt.Sprintf("; Subcategory: %s", c.Params("subcat"))
1309 }
1310 return str
1311 }(), h1.Title)
1312 h1.Page = "category"
1313 if c.Params("cat") == "" && c.Params("subcat") == "" {
1314 tmplData = map[string]interface{}{
1315 "Products": allproducts,
1316 "Page": h1,
1317 "Category": c.Params("cat"),
1318 "Subcategory": c.Params("subcat"),
1319 "Prods": allproducts,
1320 "Product": c.Params("partno"),
1321 }
1322 } else {
1323
1324 for _, prod := range allproducts {
1325 if strings.EqualFold(prod.Category, c.Params("cat")) && (c.Params("subcat") == "" || strings.EqualFold(escapesubcat(prod.Subcategory), c.Params("subcat"))) {
1326 categoryproducts = append(categoryproducts, prod)
1327 }
1328 }
1329 tmplData = map[string]interface{}{
1330 "Products": categoryproducts,
1331 "Page": h1,
1332 "Category": c.Params("cat"),
1333 "Subcategory": c.Params("subcat"),
1334 "Prods": allproducts,
1335 }
1336 }
1337 err = tmpl.Execute(&result, tmplData)
1338 if err != nil {
1339 msg := fmt.Sprintf("Error execute html template: %v", err)
1340 log.Println(msg)
1341 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1342 }
1343 _, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1344 return err
1345}
1346
1347func getcats() (cats []string) {
1348 var catsMap = make(map[string]int)
1349 for _, prod := range allproducts {
1350 catsMap[prod.Category]++
1351 }
1352 for cat := range catsMap {
1353 cats = append(cats, cat)
1354 }
1355 return cats
1356}
1357func contains(slice []string, str string) bool {
1358 for _, s := range slice {
1359 if s == str {
1360 return true
1361 }
1362 }
1363 return false
1364}
1365func getcategories(allproducts p.Products) (map[string]int, []string, map[string]map[string]int, map[string][]string) {
1366 categoryCounts := make(map[string]int)
1367 subcategoryCounts := make(map[string]map[string]int)
1368 subcategoriesByCategory := make(map[string][]string)
1369
1370 for _, prod := range allproducts {
1371 if prod.Category != "" {
1372 categoryCounts[prod.Category]++
1373 if prod.Subcategory != "" {
1374 if subcategoryCounts[prod.Category] == nil {
1375 subcategoryCounts[prod.Category] = make(map[string]int)
1376 }
1377 subcategoryCounts[prod.Category][prod.Subcategory]++
1378 if !contains(subcategoriesByCategory[prod.Category], prod.Subcategory) {
1379 subcategoriesByCategory[prod.Category] = append(subcategoriesByCategory[prod.Category], prod.Subcategory)
1380 }
1381 }
1382 }
1383 }
1384
1385 var sortableCategories []struct {
1386 Name string
1387 Count int
1388 }
1389 for cat, count := range categoryCounts {
1390 sortableCategories = append(sortableCategories, struct {
1391 Name string
1392 Count int
1393 }{Name: cat, Count: count})
1394 }
1395 sort.Slice(sortableCategories, func(i, j int) bool {
1396 return sortableCategories[i].Count > sortableCategories[j].Count
1397 })
1398 var sortedCategories []string
1399 for _, cat := range sortableCategories {
1400 sortedCategories = append(sortedCategories, cat.Name)
1401 var sortableSubcategories []struct {
1402 Name string
1403 Count int
1404 }
1405 for subcat, count := range subcategoryCounts[cat.Name] {
1406 sortableSubcategories = append(sortableSubcategories, struct {
1407 Name string
1408 Count int
1409 }{Name: subcat, Count: count})
1410 }
1411 sort.Slice(sortableSubcategories, func(i, j int) bool {
1412 return sortableSubcategories[i].Count > sortableSubcategories[j].Count
1413 })
1414 var sortedSubcategories []string
1415 for _, subcat := range sortableSubcategories {
1416 sortedSubcategories = append(sortedSubcategories, subcat.Name)
1417 }
1418 subcategoriesByCategory[cat.Name] = sortedSubcategories
1419 }
1420 return categoryCounts, sortedCategories, subcategoryCounts, subcategoriesByCategory
1421}
1422
1423func getsubcats(cat string) (subcats []string) {
1424 var subcatsMap = make(map[string]int)
1425 for _, prod := range allproducts {
1426 if cat == "" || strings.EqualFold(cat, prod.Category) {
1427 if prod.Subcategory != "" {
1428 subcatsMap[escapesubcat(prod.Subcategory)]++
1429 }
1430 }
1431 }
1432 for subcat := range subcatsMap {
1433 subcats = append(subcats, subcat)
1434 }
1435 return subcats
1436}
1437func escapesubcat(sc string) (esc string) {
1438 esc = strings.Replace(sc, "ΒΌ", "quarter-", -1)
1439 esc = strings.Replace(esc, "Β½", "half-", -1)
1440 esc = strings.Replace(esc, "1/16", "sixteenth-", -1)
1441 esc = strings.Replace(esc, "%", "-pct", -1)
1442 esc = strings.Replace(esc, " ", " ", -1)
1443 esc = strings.Replace(esc, " ", "-", -1)
1444 esc = strings.Replace(esc, "--", "-", -1)
1445 esc = strings.Replace(esc, "watt1", "watt-1", -1)
1446 esc = strings.Replace(esc, "watt5", "watt-5", -1)
1447 return esc
1448}
1449
1450func handlecat(c fiber.Ctx) error {
1451 if c.Params("cat") == "" && c.Params("subcat") == "" {
1452 return cathtmlfunc(c)
1453 }
1454 var catexists bool
1455 var subcatexists bool
1456 catexists = false
1457 for _, cat := range getcats() {
1458 if strings.EqualFold(cat, c.Params("cat")) {
1459 catexists = true
1460 break
1461 }
1462 }
1463 subcatexists = false
1464 if c.Params("subcat") != "" {
1465 for _, subcat := range getsubcats("") {
1466 if strings.EqualFold(escapesubcat(subcat), c.Params("subcat")) {
1467 subcatexists = true
1468 break
1469 }
1470 }
1471 }
1472 if c.Params("subcat") != "" && !subcatexists {
1473 log.Printf("subcategory %s does not match any existing subcategory\n", c.Params("subcat"))
1474 return c.Redirect().To("/cat/" + c.Params("cat"))
1475 }
1476 if !catexists {
1477 log.Printf("category %s does not match any existing category\n", c.Params("cat"))
1478 return c.Redirect().To("/cat")
1479 }
1480 if catexists || (catexists && subcatexists) {
1481 return cathtmlfunc(c)
1482 }
1483 return c.SendStatus(fiber.StatusNotFound)
1484}
1485
1486func homepage(c fiber.Ctx) error {
1487 tmpl, err := mainTmpl()
1488 if err != nil {
1489 msg := fmt.Sprintf("Could not parsing html template: %v", err)
1490 log.Println(msg)
1491 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1492 }
1493 tmpl0, err := tmpl.Clone()
1494 if err != nil {
1495 msg := fmt.Sprintf("Error cloning template: %v", err)
1496 log.Println(msg)
1497 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1498 }
1499 _, err = tmpl0.New("main").Parse(h.FrontPage())
1500 if err != nil {
1501 msg := fmt.Sprintf("Error parsing Front Page template: %v", err)
1502 log.Println(msg)
1503 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1504 }
1505 _, err = tmpl0.New("about").Parse(h.AboutPage())
1506 if err != nil {
1507 msg := fmt.Sprintf("Error parsing About Page template: %v", err)
1508 log.Println(msg)
1509 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1510 }
1511 _, err = tmpl0.New("policy").Parse(h.PolicyPage())
1512 if err != nil {
1513 msg := fmt.Sprintf("Error parsing Policy Page template: %v", err)
1514 log.Println(msg)
1515 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1516 }
1517 _, err = tmpl0.New("links").Parse(h.LinksPage())
1518 if err != nil {
1519 msg := fmt.Sprintf("Error parsing Links Page template: %v", err)
1520 log.Println(msg)
1521 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1522 }
1523 tmpl = tmpl0
1524 log.Println(c.Get("User-Agent"))
1525 c.Set("Content-Type", "text/html;charset=utf-8")
1526 h1 := pageMeta(c, htmlPageTemplateData)
1527 tmplData := map[string]interface{}{
1528 "Page": h1,
1529 "Prods": allproducts,
1530 }
1531 var result bytes.Buffer
1532 err = tmpl.Execute(&result, tmplData)
1533 if err != nil {
1534 msg := fmt.Sprintf("Error executing template: %v", err)
1535 log.Println(msg)
1536 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1537 }
1538 _, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1539 return err
1540}
1541
1542func productpage(c fiber.Ctx) error {
1543 tmpl, err := mainTmpl()
1544 if err != nil {
1545 msg := fmt.Sprintf("Error parsing html template: %v", err)
1546 log.Println(msg)
1547 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1548 }
1549 tmpl0, err := tmpl.Clone()
1550 if err != nil {
1551 msg := fmt.Sprintf("Error cloning template: %v", err)
1552 log.Println(msg)
1553 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1554 }
1555 _, err = tmpl0.New("main").Parse(h.ProductPage())
1556 if err != nil {
1557 msg := fmt.Sprintf("Error parsing product page template: %v", err)
1558 log.Println(msg)
1559 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1560 }
1561 tmpl = tmpl0
1562 c.Set("Content-Type", "text/html;charset=utf-8")
1563 for _, prod := range allproducts {
1564 if strings.EqualFold(prod.Partno, c.Params("partno")) {
1565 var result bytes.Buffer
1566 h1 := pageMeta(c, htmlPageTemplateData)
1567 h1.Page = "product"
1568 h1.Title = fmt.Sprintf("%s | %s", prod.Name, h1.Title)
1569 tmplData := map[string]interface{}{
1570 "Prod": prod,
1571 "Page": h1,
1572 "Prods": allproducts,
1573 }
1574 err := tmpl.Execute(&result, tmplData)
1575 if err != nil {
1576 log.Println("error: ", err)
1577 _, err = c.Status(fiber.StatusInternalServerError).Write(result.Bytes())
1578 return err
1579 }
1580 _, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1581 return err
1582 }
1583 }
1584 log.Printf("product %s does not match any existing product\n", c.Params("partno"))
1585 return c.Status(fiber.StatusNotFound).Redirect().To("/cat")
1586}
1587
1588
1589// ===== order.go =====
1590// Package main order.go
1591package main
1592
1593import (
1594 "bytes"
1595 "encoding/json"
1596 "fmt"
1597 htmpl "html/template"
1598 "log"
1599 "os"
1600 "path/filepath"
1601 "regexp"
1602 "strconv"
1603 "strings"
1604 "time"
1605
1606 "github.com/bitfield/script"
1607 "github.com/gofiber/fiber/v3"
1608 "github.com/stripe/stripe-go/v81"
1609 "github.com/stripe/stripe-go/v81/paymentintent"
1610)
1611
1612// validPIID matches Stripe PaymentIntent IDs: "pi_" followed by alphanumeric chars.
1613// Also allows plain alphanumeric+underscore+hyphen for test order IDs.
1614var validPIID = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
1615
1616func handleOrder(r *fiber.App) {
1617 r.Get("/checkout.css", func(c fiber.Ctx) error {
1618 c.Set("Content-Type", "text/css;charset=utf-8")
1619 _, err := c.Status(fiber.StatusOK).Write([]byte(h.CheckoutCSS()))
1620 return err
1621 })
1622
1623 r.Get("/complete", func(c fiber.Ctx) error {
1624 // Complete template
1625 completetmpl := htmpl.New("index")
1626 if _, err := completetmpl.Parse(h.CompletePage()); err != nil {
1627 msg := fmt.Sprintf("Error parsing complete page template: %v", err)
1628 log.Println(msg)
1629 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1630 }
1631 if _, err := completetmpl.New("wasm").Parse(h.Wasm()); err != nil {
1632 log.Println("Error parsing wasm template:", err)
1633 msg := fmt.Sprintf("Error parsing wasm template: %v", err)
1634 log.Println(msg)
1635 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1636 }
1637 h1 := htmlPageTemplateData
1638 /*
1639 proto := "http"
1640 if c.Secure() {
1641 proto += "s"
1642 }
1643 */
1644 proto := "https"
1645 h1.Canonical = proto + `://` + c.Hostname() + c.OriginalURL()
1646 h1.BaseURL = proto + `://` + c.Hostname()
1647 h1.RequestHost = c.Hostname()
1648 h1.Protocol = proto
1649 h1.Time = time.Now().Format(time.RFC3339Nano)
1650 h1.Year = fmt.Sprintf("%v", time.Now().Year())
1651 tmplData := map[string]interface{}{
1652 "Page": h1,
1653 }
1654 var result bytes.Buffer
1655 err := completetmpl.Execute(&result, tmplData)
1656 if err != nil {
1657 msg := fmt.Sprintf("Could not execute html template %v", err)
1658 log.Println(msg)
1659 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1660 }
1661 c.Set("Content-Type", "text/html;charset=utf-8")
1662 return c.Status(fiber.StatusOK).Send(result.Bytes())
1663 })
1664
1665 r.Get("/order/:piid", func(c fiber.Ctx) error {
1666 piid := c.Params("piid")
1667 if !validPIID.MatchString(piid) {
1668 return c.Status(fiber.StatusBadRequest).SendString("Invalid order ID")
1669 }
1670 order, err := script.File("orders/" + piid + ".json").Bytes()
1671 if err != nil {
1672 return c.Status(fiber.StatusNotFound).SendString("Order not found")
1673 }
1674 return c.Status(fiber.StatusOK).Send(order)
1675 })
1676
1677 r.Get("/order/:piid/html", func(c fiber.Ctx) error {
1678 piid := c.Params("piid")
1679 if !validPIID.MatchString(piid) {
1680 return c.Status(fiber.StatusBadRequest).SendString("Invalid order ID")
1681 }
1682 order, err := script.File("orders/" + piid + ".json").Bytes()
1683 if err != nil {
1684 return c.Status(fiber.StatusNotFound).SendString("Order not found")
1685 }
1686 var m map[string]interface{}
1687 if err := json.Unmarshal(order, &m); err != nil {
1688 return c.Status(500).SendString("failed to unmarshal order json: " + err.Error())
1689 }
1690 receipt, err := buildReceipt(m, piid)
1691 if err != nil {
1692 return c.Status(500).SendString("failed to build receipt: " + err.Error())
1693 }
1694 return c.Status(200).SendString(string(receipt))
1695 })
1696
1697 r.Post("/create-payment-intent", func(c fiber.Ctx) error {
1698 rawBody := c.Body()
1699 if rawBody == nil {
1700 log.Printf("Failed to read raw request body")
1701 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to read request body"})
1702 }
1703
1704 var req struct {
1705 Items []item `json:"items"`
1706 }
1707 if err := json.Unmarshal(rawBody, &req); err != nil {
1708 log.Printf("Failed to parse JSON: %v", err)
1709 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
1710 }
1711
1712 if len(req.Items) == 0 {
1713 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No items in request"})
1714 }
1715
1716 // Validate each item's amount against the server-side product catalog.
1717 // Client sends ID as "partno X qty" for products, or "shipping-to|..." for shipping.
1718 total := int64(0)
1719 for _, it := range req.Items {
1720 if it.Amount <= 0 {
1721 log.Printf("Rejected item with non-positive amount: %s = %d", it.ID, it.Amount)
1722 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid item amount"})
1723 }
1724 if strings.HasPrefix(it.ID, "shipping-to|") {
1725 // Shipping line β accept the client-supplied amount
1726 total += it.Amount
1727 continue
1728 }
1729 // Extract partno and qty from "partno X qty"
1730 expectedAmt, err := validateItemAmount(it.ID, it.Amount)
1731 if err != nil {
1732 log.Printf("Item validation failed for %q: %v", it.ID, err)
1733 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Item validation failed"})
1734 }
1735 total += expectedAmt
1736 }
1737
1738 if total < 50 {
1739 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Order total must be at least $0.50"})
1740 }
1741
1742 params := &stripe.PaymentIntentParams{
1743 Amount: stripe.Int64(total),
1744 Currency: stripe.String(string(stripe.CurrencyUSD)),
1745 }
1746 pi, err := paymentintent.New(params)
1747 if err != nil {
1748 log.Printf("Failed to create PaymentIntent: %v", err)
1749 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
1750 }
1751
1752 log.Printf("Created PaymentIntent %s for %d cents", pi.ID, total)
1753 return c.Status(fiber.StatusOK).JSON(fiber.Map{
1754 "clientSecret": pi.ClientSecret,
1755 "dpmCheckerLink": fmt.Sprintf("https://dashboard.stripe.com/settings/payment_methods/review?transaction_id=%s", pi.ID),
1756 })
1757 })
1758
1759 r.Post("/submit-order", func(c fiber.Ctx) error {
1760 var requestData struct {
1761 LocalStorageData map[string]interface{} `json:"localStorageData"`
1762 PaymentIntentID string `json:"paymentIntentId"`
1763 }
1764
1765 if err := c.Bind().Body(&requestData); err != nil {
1766 log.Println(err)
1767 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request data"})
1768 }
1769
1770 if !validPIID.MatchString(requestData.PaymentIntentID) {
1771 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payment intent ID"})
1772 }
1773
1774 log.Printf("Received payment intent ID: %s\n", requestData.PaymentIntentID)
1775
1776 paymentIntent, err := paymentintent.Get(requestData.PaymentIntentID, nil)
1777 if err != nil {
1778 log.Printf("Error retrieving payment intent: %v", err)
1779 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to verify payment"})
1780 }
1781 if paymentIntent.Status != stripe.PaymentIntentStatusSucceeded {
1782 log.Printf("Payment was not successful, status: %s", paymentIntent.Status)
1783 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Payment not successful"})
1784 }
1785
1786 ordersDir := "./orders"
1787 if err := os.MkdirAll(ordersDir, os.ModePerm); err != nil {
1788 log.Printf("Error creating orders directory: %v", err)
1789 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1790 }
1791
1792 filePath := filepath.Join(ordersDir, fmt.Sprintf("%s.json", requestData.PaymentIntentID))
1793
1794 // Idempotency: if the order file already exists, don't overwrite or reprint
1795 if _, err := os.Stat(filePath); err == nil {
1796 log.Printf("Order %s already exists, skipping duplicate submission", requestData.PaymentIntentID)
1797 return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Order already submitted"})
1798 }
1799
1800 // Include the verified Stripe amount alongside the client-supplied data
1801 orderData := map[string]interface{}{
1802 "clientData": requestData.LocalStorageData,
1803 "verifiedCents": paymentIntent.Amount,
1804 "currency": string(paymentIntent.Currency),
1805 "stripeStatus": string(paymentIntent.Status),
1806 "submittedAt": time.Now().Format(time.RFC3339),
1807 }
1808
1809 data, err := json.MarshalIndent(orderData, "", " ")
1810 if err != nil {
1811 log.Printf("Error marshaling data to json: %v", err)
1812 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1813 }
1814 if err := os.WriteFile(filePath, data, 0o644); err != nil {
1815 log.Printf("Error writing data to file: %v", err)
1816 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1817 }
1818
1819 // ---- Print receipt via CUPS (non-blocking so your response is snappy)
1820 go func(pid string, local map[string]interface{}) {
1821 receipt, err := buildReceipt(local, pid)
1822 if err != nil {
1823 log.Printf("build receipt failed: %v", err)
1824 return
1825 }
1826 if err := sendToCUPS(receipt, "Order "+pid); err != nil {
1827 log.Printf("print failed: %v", err)
1828 _ = os.WriteFile(filepath.Join(ordersDir, pid+".print_failed"), []byte(err.Error()), 0o644)
1829 }
1830 }(requestData.PaymentIntentID, requestData.LocalStorageData)
1831
1832 return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Order submitted successfully"})
1833 })
1834
1835 /*
1836 r.Post("/reprint/:pid", func(c fiber.Ctx) error {
1837 pid := c.Params("pid")
1838 b, err := os.ReadFile(filepath.Join("./orders", pid+".json"))
1839 if err != nil { return c.Status(404).SendString("not found") }
1840 var m map[string]interface{}
1841 if err := json.Unmarshal(b, &m); err != nil { return c.Status(500).SendString(err.Error()) }
1842 receipt, err := buildReceipt(m, pid)
1843 if err != nil { return c.Status(500).SendString(err.Error()) }
1844 if err := sendToCUPS(receipt, "Order "+pid); err != nil {
1845 return c.Status(500).SendString(err.Error())
1846 }
1847 return c.SendStatus(204)
1848 })
1849 */
1850}
1851
1852func buildReceipt(local map[string]interface{}, paymentIntentID string) ([]byte, error) {
1853 // Pretty JSON body from what you already persisted
1854 body, err := json.MarshalIndent(local, "", " ")
1855 if err != nil {
1856 return nil, err
1857 }
1858 // Simple text receipt header
1859 ts := time.Now().Format("2006-01-02 15:04:05")
1860 hdr := fmt.Sprintf(
1861 "==================== ORDER ====================\n"+
1862 "PaymentIntent: %s\nTime: %s\n===============================================\n\n",
1863 paymentIntentID, ts,
1864 )
1865 // Footer (optional)
1866 ftr := "\n\n---------------------- END ---------------------\n"
1867 receipt := append([]byte(hdr), body...)
1868 receipt = append(receipt, []byte(ftr)...)
1869 return receipt, nil
1870}
1871
1872// serverPriceCents looks up a product's price from the server-side catalog by part number.
1873func serverPriceCents(partno string) (int64, error) {
1874 allproductsMu.RLock()
1875 prods := allproducts
1876 allproductsMu.RUnlock()
1877 for _, prod := range prods {
1878 if prod.Partno == partno {
1879 return parsePriceCents(prod.Price), nil
1880 }
1881 }
1882 return 0, fmt.Errorf("product %q not found in catalog", partno)
1883}
1884
1885// parsePriceCents converts a price string like "$1.23" or "1.23" to cents.
1886func parsePriceCents(s string) int64 {
1887 if s == "" {
1888 return 0
1889 }
1890 s = strings.TrimPrefix(s, "$")
1891 f, err := strconv.ParseFloat(s, 64)
1892 if err != nil {
1893 return 0
1894 }
1895 if f < 0 {
1896 return -int64(-f*100 + 0.5)
1897 }
1898 return int64(f*100 + 0.5)
1899}
1900
1901// validateItemAmount parses a client item ID ("partno X qty"), looks up the
1902// server-side price, computes the expected total, and returns it. If the
1903// client-supplied amount doesn't match, an error is returned.
1904func validateItemAmount(itemID string, clientAmount int64) (int64, error) {
1905 // Parse "partno X qty"
1906 parts := strings.SplitN(itemID, " X ", 2)
1907 if len(parts) != 2 {
1908 return 0, fmt.Errorf("unexpected item ID format: %q", itemID)
1909 }
1910 partno := parts[0]
1911 qty, err := strconv.Atoi(parts[1])
1912 if err != nil || qty <= 0 {
1913 return 0, fmt.Errorf("invalid quantity in item ID %q", itemID)
1914 }
1915
1916 unitCents, err := serverPriceCents(partno)
1917 if err != nil {
1918 return 0, err
1919 }
1920 expected := unitCents * int64(qty)
1921 if expected != clientAmount {
1922 return 0, fmt.Errorf("amount mismatch for %q: client sent %d cents, server expects %d cents", partno, clientAmount, expected)
1923 }
1924 return expected, nil
1925}
1926
1927// escape for inclusion inside *double quotes* in a bash command string
1928func bashEscapeDoubleQuoted(s string) string {
1929 s = strings.ReplaceAll(s, `\`, `\\`)
1930 s = strings.ReplaceAll(s, `"`, `\"`)
1931 s = strings.ReplaceAll(s, "$", `\$`)
1932 s = strings.ReplaceAll(s, "`", "\\`")
1933 return s
1934}
1935
1936func sendToCUPS(receipt []byte, title string) error {
1937 if title == "" {
1938 title = "Order"
1939 }
1940 var cmd strings.Builder
1941 cmd.WriteString("lp")
1942
1943 if f.PrinterName != "" {
1944 cmd.WriteString(` -d "`)
1945 cmd.WriteString(bashEscapeDoubleQuoted(f.PrinterName))
1946 cmd.WriteString(`"`)
1947 }
1948
1949 cmd.WriteString(` -t "`)
1950 cmd.WriteString(bashEscapeDoubleQuoted(title))
1951 cmd.WriteString(`"`)
1952
1953 if f.CupsOptions != "" {
1954 for _, opt := range strings.Split(f.CupsOptions, ",") {
1955 opt = strings.TrimSpace(opt)
1956 if opt == "" {
1957 continue
1958 }
1959 cmd.WriteString(` -o "`)
1960 cmd.WriteString(bashEscapeDoubleQuoted(opt))
1961 cmd.WriteString(`"`)
1962 }
1963 }
1964
1965 full := fmt.Sprintf(`bash -lc %q`, cmd.String())
1966
1967 _, err := script.Echo(string(receipt)).Exec(full).Stdout()
1968 if err != nil {
1969 return fmt.Errorf("lp failed: %v", err)
1970 }
1971 return nil
1972}
1973
1974
1975// ===== other.go =====
1976// Package main other.go
1977package main
1978
1979import (
1980 "bytes"
1981 "fmt"
1982 "log"
1983
1984 "github.com/gofiber/fiber/v3"
1985)
1986
1987func handleOthers(r *fiber.App) {
1988 r.Get("/COVID", func(c fiber.Ctx) error {
1989 tmpl, err := mainTmpl()
1990 if err != nil {
1991 msg := fmt.Sprintf("Error parse html template: %v", err)
1992 log.Println(msg)
1993 return c.Status(fiber.StatusInternalServerError).SendString(msg)
1994 }
1995 tmpl0, err := tmpl.Clone()
1996 if err != nil {
1997 msg := fmt.Sprintf("Error cloning template: %v", err)
1998 log.Println(msg)
1999 return c.Status(fiber.StatusInternalServerError).SendString(msg)
2000 }
2001 _, err = tmpl0.New("main").Parse(h.COVIDPage())
2002 if err != nil {
2003 msg := fmt.Sprintf("Error parsing main template: %v", err)
2004 log.Println(msg)
2005 return c.Status(fiber.StatusInternalServerError).SendString(msg)
2006 }
2007 tmpl = tmpl0
2008 log.Println(c.Get("User-Agent"))
2009 c.Set("Content-Type", "text/html;charset=utf-8")
2010 h1 := pageMeta(c, htmlPageTemplateData)
2011 h1.Page = "hidden"
2012 h1.MetaDesc = "The COVID ΜΆvΜΆaΜΆcΜΆcΜΆiΜΆnΜΆeΜΆ bioweapon injection genocide and the new dark age of humanity"
2013 // h1.Mobile = strings.Contains(strings.ToLower(c.Get("User-Agent")), "mobile")
2014 tmplData := map[string]interface{}{
2015 "Page": h1,
2016 "Prods": allproducts,
2017 }
2018 var result bytes.Buffer
2019 err = tmpl.Execute(&result, tmplData)
2020 if err != nil {
2021 msg := fmt.Sprintf("Error executing template: %v", err)
2022 log.Println(msg)
2023 return c.Status(fiber.StatusInternalServerError).SendString(msg)
2024 }
2025 _, 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))
2026 return err
2027 })
2028
2029}
2030
2031
2032// ===== source.go =====
2033// Package main source.go
2034package main
2035
2036import (
2037 "bytes"
2038 "embed"
2039 "fmt"
2040 "io/fs"
2041 "os"
2042 "strings"
2043
2044 "github.com/alecthomas/chroma/v2"
2045 "github.com/alecthomas/chroma/v2/formatters/html"
2046 "github.com/alecthomas/chroma/v2/lexers"
2047 "github.com/alecthomas/chroma/v2/styles"
2048 "github.com/gofiber/fiber/v3"
2049)
2050
2051//go:embed *.go
2052var quine embed.FS
2053
2054var sourceWasm = os.DirFS("wasm")
2055var sourcesWasm []fs.FS
2056var sourceCore = os.DirFS("ui")
2057var sourceHtml = os.DirFS("htmpl")
2058var sourceContent = os.DirFS("content")
2059
2060func serveSourceCode(r *fiber.App) {
2061 for _, wasmSRC := range f.WasmSRC {
2062 sourcesWasm = append(sourcesWasm, os.DirFS(wasmSRC))
2063 }
2064 r.Get("/sourcecode", func(c fiber.Ctx) error {
2065 ret := `<!doctype html>
2066<html lang='en'>
2067<head>
2068<link rel="stylesheet" href="/style.css" type="text/css">
2069</head>
2070<body class='grid-container' style='background-color:black;color:white;'>
2071<a href='/sourcecode/go'>GO</a><br><br>
2072
2073<a href='/sourcecode/html'>HTML</a><br><br>
2074
2075<a href='/sourcecode/content'>Content</a><br><br>
2076
2077<a href='/sourcecode/core'>C.O.R.E.</a><br><br>
2078
2079<a href='/sourcecode/wasm'>WASM</a><br><br>
2080
2081</body>
2082</html>
2083`
2084 c.Set("Content-Type", "text/html;charset=utf-8")
2085 _, err := c.Status(fiber.StatusOK).Write([]byte(ret))
2086 return err
2087 })
2088
2089 r.Get("/sourcecode/html", sourcecodehtml)
2090 r.Get("/sourcecode/content", sourcecodecontent)
2091 r.Get("/sourcecode/go", sourcecodego)
2092 r.Get("/sourcecode/core", sourcecodecore)
2093 // r.Get("/sourcecodewasm", sourcecodewasm)
2094 r.Get("/sourcecode/wasm", func(c fiber.Ctx) error {
2095 ret := `<!doctype html>
2096<html lang='en'>
2097<head>
2098<link rel="stylesheet" href="/style.css" type="text/css">
2099</head>
2100<body class='grid-container' style='background-color:black;color:white;'>
2101`
2102 for _, wasmSRC := range f.WasmSRC {
2103 pathNameSlc := strings.Split(wasmSRC, "/")
2104 pathName := pathNameSlc[len(pathNameSlc)-1]
2105 ret += `<a href='/sourcecode/wasm/` + pathName + `'>` + pathName + `</a><br>
2106 `
2107 }
2108 ret += `</body></html>
2109 `
2110 c.Set("Content-Type", "text/html;charset=utf-8")
2111 _, err := c.Status(fiber.StatusOK).Write([]byte(ret))
2112 return err
2113 })
2114
2115 for i, wasmSRC := range f.WasmSRC {
2116 pathNameSlc := strings.Split(wasmSRC, "/")
2117 pathName := pathNameSlc[len(pathNameSlc)-1]
2118 r.Get("/sourcecode/wasm/"+pathName, func(c fiber.Ctx) error {
2119 return sourcecode(c, sourcesWasm[i], "dracula", "go")
2120 })
2121 }
2122}
2123
2124func sourcecodehtml(c fiber.Ctx) error {
2125 return sourcecode(c, sourceHtml, "monokai", "html")
2126}
2127func sourcecodecontent(c fiber.Ctx) error {
2128 return sourcecode(c, sourceContent, "monokai", "html")
2129}
2130func sourcecodego(c fiber.Ctx) error {
2131 return sourcecode(c, quine, "monokai", "go")
2132}
2133
2134func sourcecodewasm(c fiber.Ctx) error {
2135 return sourcecode(c, sourceWasm, "dracula", "go")
2136}
2137
2138func sourcecodecore(c fiber.Ctx) error {
2139 return sourcecode(c, sourceCore, "solarized-dark256", "go")
2140}
2141
2142func sourcecode(c fiber.Ctx, fsys fs.FS, styleName string, lang string) error {
2143 c.Set("Content-Type", "text/html;charset=utf-8")
2144 var buf bytes.Buffer
2145 var builder strings.Builder
2146
2147 fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
2148 if err != nil {
2149 return err
2150 }
2151 if !d.IsDir() && strings.HasSuffix(path, "."+lang) {
2152 content, err := fs.ReadFile(fsys, path)
2153 if err != nil {
2154 return err
2155 }
2156 builder.WriteString(fmt.Sprintf("// ===== %s =====\n", path))
2157 builder.Write(content)
2158 builder.WriteString("\n\n")
2159 }
2160 return nil
2161 })
2162
2163 // Pick lexer & style
2164 lexer := lexers.Get(lang)
2165 if lexer == nil {
2166 lexer = lexers.Fallback
2167 }
2168 lexer = chroma.Coalesce(lexer)
2169
2170 style := styles.Get(styleName)
2171 if style == nil {
2172 style = styles.Fallback
2173 }
2174
2175 // Formatter with line numbers & CSS classes
2176 formatter := html.New(
2177 html.WithLineNumbers(true),
2178 html.WithClasses(true),
2179 )
2180
2181 iterator, err := lexer.Tokenise(nil, builder.String())
2182 if err != nil {
2183 return err
2184 }
2185
2186 // Optional: include CSS in output
2187 var css bytes.Buffer
2188 _ = formatter.WriteCSS(&css, style)
2189 buf.WriteString("<style>")
2190 buf.Write(css.Bytes())
2191 buf.WriteString("</style>")
2192
2193 if err := formatter.Format(&buf, style, iterator); err != nil {
2194 return err
2195 }
2196
2197 _, err = c.Status(fiber.StatusOK).Write(buf.Bytes())
2198 return err
2199}
2200
2201
2202// ===== tmpl.go =====
2203// Package main tmpl.go
2204package main
2205
2206import (
2207 "bytes"
2208 "fmt"
2209 htmpl "html/template"
2210 "log"
2211 "os"
2212 "path/filepath"
2213 "sort"
2214 "strconv"
2215 "strings"
2216 ttmpl "text/template"
2217 "time"
2218
2219 p "github.com/0magnet/m2/pkg/product"
2220 "github.com/gofiber/fiber/v3"
2221)
2222
2223/*
2224//go:embed htmpl/*
2225var templatesFS embed.FS
2226
2227//go:embed content/*
2228var contentFS embed.FS
2229*/
2230/*
2231var (
2232 templatesFS = os.DirFS("htmpl")
2233 contentFS = os.DirFS("content")
2234)
2235*/
2236/*
2237func mustReadEmbeddedFileToString(path string, fs embed.FS) string {
2238 return string(mustReadEmbeddedFileToBytes(path, fs))
2239}
2240
2241func mustReadEmbeddedFileToBytes(path string, fs embed.FS) []byte {
2242 data, err := fs.ReadFile(path)
2243 if err != nil {
2244 panic(err)
2245 }
2246 return data
2247}
2248*/
2249
2250func mustReadFileToString(path string) string {
2251 return string(mustReadFileToBytes(path))
2252}
2253
2254func mustReadFileToBytes(path string) []byte {
2255 data, err := os.ReadFile(path) //nolint
2256 if err != nil {
2257 panic(err)
2258 }
2259 return data
2260}
2261
2262type htmlTemplate struct {
2263 Empty func() string
2264 Head func() string
2265 Logo func() string
2266 Header func() string
2267 Categories func() string
2268 CatSubcats func() string
2269 Footer func() string
2270 MainPage func() string
2271 AuxPage func() string
2272 FrontPage func() string
2273 CategoryPage func() string
2274 CategoryPageMD func() string
2275 ProductPage func() string
2276 ProductPageMD func() string
2277 Schema func() string
2278 Cart func() string
2279 XMLSitemap func() string
2280 Wasm func() string
2281 Clock func() string
2282 AboutPage func() string
2283 PolicyPage func() string
2284 LinksPage func() string
2285 CheckoutPage func() string
2286 CompletePage func() string
2287 CheckoutCSS func() string
2288 StyleCSS func() string
2289 COVIDPage func() string
2290}
2291
2292var h = htmlTemplate{
2293 Empty: func() string { return mustReadFileToString("htmpl/empty.html") },
2294 Head: func() string { return mustReadFileToString("htmpl/head.html") },
2295 Logo: func() string { return mustReadFileToString("htmpl/logo.html") },
2296 Header: func() string { return mustReadFileToString("htmpl/header.html") },
2297 Categories: func() string { return mustReadFileToString("htmpl/categories.html") },
2298 CatSubcats: func() string { return mustReadFileToString("htmpl/catsubcats.html") },
2299 Footer: func() string { return mustReadFileToString("htmpl/footer.html") },
2300 MainPage: func() string { return mustReadFileToString("htmpl/main.html") },
2301 AuxPage: func() string { return mustReadFileToString("htmpl/aux.html") },
2302 FrontPage: func() string { return mustReadFileToString("htmpl/front.html") },
2303 CategoryPage: func() string { return mustReadFileToString("htmpl/category.html") },
2304 CategoryPageMD: func() string { return mustReadFileToString("htmpl/category.md") },
2305 ProductPage: func() string { return mustReadFileToString("htmpl/product.html") },
2306 ProductPageMD: func() string { return mustReadFileToString("htmpl/product.md") },
2307 Schema: func() string { return mustReadFileToString("htmpl/schema.html") },
2308 Cart: func() string { return mustReadFileToString("htmpl/cart.html") },
2309 XMLSitemap: func() string { return mustReadFileToString("htmpl/sitemap.xml") },
2310 Wasm: func() string { return mustReadFileToString("htmpl/wasm.html") },
2311 CompletePage: func() string { return mustReadFileToString("htmpl/complete.html") },
2312 Clock: func() string { return mustReadFileToString("content/clock.html") },
2313 AboutPage: func() string { return mustReadFileToString("content/about.html") },
2314 PolicyPage: func() string { return mustReadFileToString("content/policy.html") },
2315 LinksPage: func() string { return mustReadFileToString("content/links.html") },
2316 CheckoutPage: func() string { return mustReadFileToString("content/checkout.html") },
2317 CheckoutCSS: func() string { return mustReadFileToString("content/checkout.css") },
2318 StyleCSS: func() string { return mustReadFileToString("content/style.css") },
2319 COVIDPage: func() string { return mustReadFileToString("content/mementomori.html") },
2320}
2321
2322var htmlPageTemplateData htmlTemplateData
2323
2324var funcs = htmpl.FuncMap{
2325 "replace": replace, "mul": mul, "div": div, "safeHTML": safeHTML,
2326 "safeJS": safeJS, "stripProtocol": stripProtocol, "add": add, "sub": sub,
2327 "toFloat": toFloat, "equalsIgnoreCase": equalsIgnoreCase,
2328 "getsubcats": getsubcats, "escapesubcat": escapesubcat,
2329 "sortsubcats": sortsubcats, "repeat": repeat, "subcatlink": subcatlink,
2330}
2331
2332func mainTmpl() (tmpl *htmpl.Template, err error) {
2333 tmpl = htmpl.New("index").Funcs(funcs)
2334 if _, err := tmpl.Parse(h.MainPage()); err != nil {
2335 log.Println("Error parsing index template:", err)
2336 return tmpl, err
2337 }
2338
2339 partials := []struct {
2340 Name string
2341 Content string
2342 }{
2343 {"head", h.Head()},
2344 {"schema", h.Schema()},
2345 {"header", h.Header()},
2346 {"catsubcats", h.CatSubcats()},
2347 {"categories", h.Categories()},
2348 {"footer", h.Footer()},
2349 {"cart", h.Cart()},
2350 {"wasm", h.Wasm()},
2351 }
2352
2353 for _, p := range partials {
2354 if _, err := tmpl.New(p.Name).Parse(p.Content); err != nil {
2355 log.Printf("Error parsing %s template: %v", p.Name, err)
2356 return tmpl, err
2357 }
2358 }
2359 return tmpl, err
2360}
2361
2362func auxTmpl() (tmpl *htmpl.Template, err error) {
2363 tmpl = htmpl.New("index").Funcs(funcs)
2364 if _, err := tmpl.Parse(h.AuxPage()); err != nil {
2365 log.Println("Error parsing index template:", err)
2366 return tmpl, err
2367 }
2368
2369 partials := []struct {
2370 Name string
2371 Content string
2372 }{
2373 {"head", h.Head()},
2374 {"schema", h.Empty()},
2375 {"wasm", h.Empty()},
2376 }
2377
2378 for _, p := range partials {
2379 if _, err := tmpl.New(p.Name).Parse(p.Content); err != nil {
2380 log.Printf("Error parsing %s template: %v", p.Name, err)
2381 return tmpl, err
2382 }
2383 }
2384 return tmpl, err
2385}
2386
2387func pageMeta(c fiber.Ctx, base htmlTemplateData) htmlTemplateData {
2388 h := base
2389 host := string(c.Request().Host())
2390 /*
2391 proto := "http"
2392 if c.Secure() {
2393 proto += "s"
2394 }
2395 */
2396 proto := "https"
2397 h.Canonical = proto + "://" + host + c.OriginalURL()
2398 h.BaseURL = proto + "://" + host
2399 h.RequestHost = host
2400 h.Protocol = proto
2401 h.CatsCounts, h.Cats, h.SubCatsCounts, h.SubCatsByCat = getcategories(allproducts)
2402 h.LenAllProducts = len(allproducts)
2403 h.Time = time.Now().Format(time.RFC3339Nano)
2404 h.Year = fmt.Sprintf("%v", time.Now().Year())
2405 h.MetaDesc = f.Sitemeta
2406 h.KeyWords = strings.Replace(f.Sitelongname, " ", ", ", -1)
2407 return h
2408}
2409
2410func initTMPL() {
2411 htmlPageTemplateData = htmlTemplateData{
2412 NoCore: f.NoCore,
2413 TestMode: f.Teststripekey,
2414 Title: f.Sitelongname,
2415 StripePK: f.StripePK,
2416 SiteName: f.Sitedomain,
2417 SiteTagLine: f.Sitetagline,
2418 SiteName1: htmpl.HTML(checkerBoard(f.Sitedomain)), //nolint
2419 SiteLongName: f.Sitelongname,
2420 SiteASCIILogo: htmpl.HTML(f.SiteASCIILogo), //nolint
2421 SitePrettyName: f.Siteprettyname,
2422 SitePrettyNameCap: f.Siteprettynamecap,
2423 SitePrettyNameCaps: f.Siteprettynamecaps,
2424 TelegramContact: f.Tgcontact,
2425 TelegramChannel: f.Tgchannel,
2426 WasmExecPath: f.WasmExecPath,
2427 WasmExecRel: f.WasmExecPath,
2428 Cats: getcats(),
2429 LenAllProducts: len(allproducts),
2430 ImgSRC: func() (ret string) {
2431 ret = f.Siteimagesrc
2432 if ret == "" {
2433 ret = "/i"
2434 }
2435 return ret
2436 }(),
2437 Page: "front",
2438 Time: time.Now().Format(time.RFC3339Nano),
2439 Year: fmt.Sprintf("%v", time.Now().Year()),
2440 }
2441 htmlPageTemplateData.CatsCounts, htmlPageTemplateData.Cats, htmlPageTemplateData.SubCatsCounts, htmlPageTemplateData.SubCatsByCat = getcategories(allproducts)
2442 htmlPageTemplateData.WasmBinary = wasmBinary()
2443
2444}
2445
2446func wasmBinary() (ret []string) {
2447 if len(f.WasmSRC) == 0 {
2448 return ret
2449 }
2450 if f.UseTinygo {
2451 for _, wasmSRC := range f.WasmSRC {
2452 outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + "-tiny.wasm"
2453 ret = append(ret, outputFile)
2454 }
2455 return ret
2456 }
2457 for _, wasmSRC := range f.WasmSRC {
2458 outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + ".wasm"
2459 ret = append(ret, outputFile)
2460 }
2461 return ret
2462}
2463
2464type xmlTemplateData struct {
2465 Cats []string
2466 SubCatsByCat map[string][]string
2467 Products p.Products
2468 Update string
2469}
2470
2471func generateSitemapXML() string {
2472 xmlSitemapTemplateData := xmlTemplateData{
2473 Products: allproducts,
2474 Update: time.Now().Format("2006-01-02"),
2475 }
2476 _, xmlSitemapTemplateData.Cats, _, xmlSitemapTemplateData.SubCatsByCat = getcategories(allproducts)
2477 var err1 error
2478 xtmpl, err1 := ttmpl.New("index").Funcs(ttmpl.FuncMap{"getsubcats": getsubcats}).Parse(h.XMLSitemap())
2479 if err1 != nil {
2480 log.Println("Error parsing index template:", err1)
2481 }
2482 var result bytes.Buffer
2483 err1 = xtmpl.Execute(&result, xmlSitemapTemplateData)
2484 if err1 != nil {
2485 log.Println("error: ", err1)
2486 }
2487 return result.String()
2488}
2489
2490func toFloat(s string) float64 {
2491 if s == "" {
2492 return 0.0
2493 }
2494 f, err := strconv.ParseFloat(s, 64)
2495 if err != nil {
2496 return 0.0
2497 }
2498 return f
2499}
2500
2501func checkerBoard(input string) string {
2502 var result strings.Builder
2503 for i, char := range input {
2504 // Wrap every other letter with the specified HTML
2505 if i%2 == 0 {
2506 result.WriteString(fmt.Sprintf("<span class='nv'>%c</span>", char))
2507 } else {
2508 result.WriteRune(char)
2509 }
2510 }
2511 return result.String()
2512}
2513
2514type htmlTemplateData struct {
2515 Title string
2516 MetaDesc string
2517 Canonical string
2518 BaseURL string
2519 ImgSRC string // url where images are hosted
2520 OrdersURL string // url where checkout is served from
2521 SiteName string
2522 SiteTagLine string
2523 SiteName1 htmpl.HTML //checkerboard - alternate swap text & bg color
2524 SiteLongName string
2525 SitePrettyName string //ππππππ₯π π€π‘πππ£π.πππ₯
2526 SitePrettyNameCap string //ππππππ₯π π€π‘πππ£π.πππ₯
2527 SitePrettyNameCaps string //ππΈπΎβπΌπππββπΌβπΌ.βπΌπ
2528 SiteASCIILogo htmpl.HTML
2529 TelegramContact string
2530 TelegramChannel string
2531 Protocol string
2532 RequestHost string
2533 KeyWords string
2534 Style htmpl.HTML
2535 Heading htmpl.HTML
2536 StripePK string
2537 Cats []string
2538 CatsCounts map[string]int
2539 SubCatsCounts map[string]map[string]int
2540 SubCatsByCat map[string][]string
2541 LenAllProducts int
2542 Mobile bool
2543 Gocanvas htmpl.HTML
2544 WasmBinary []string
2545 WasmExecPath string
2546 WasmExecRel string
2547 StyleFontFace htmpl.CSS
2548 Message htmpl.HTML
2549 Page string
2550 Year string
2551 Time string
2552 AboutHTML htmpl.HTML
2553 LinksHTML htmpl.HTML
2554 PolicyHTML htmpl.HTML
2555 TestMode bool
2556 NoCore bool
2557}
2558
2559func equalsIgnoreCase(a, b string) bool {
2560 return strings.EqualFold(strings.Join(strings.Fields(a), ""), strings.Join(strings.Fields(b), ""))
2561}
2562
2563func replace(s, o, n string) string {
2564 return strings.ReplaceAll(s, o, n)
2565}
2566func mul(a, b float64) float64 {
2567 return a * b
2568}
2569func div(a, b float64) float64 {
2570 return a / b
2571}
2572func add(a, b int) int {
2573 return a + b
2574}
2575func sub(a, b int) int {
2576 return a - b
2577}
2578func safeHTML(s string) htmpl.HTML {
2579 return htmpl.HTML(s) //nolint
2580}
2581func safeJS(s string) htmpl.JS {
2582 return htmpl.JS(s) //nolint
2583}
2584func stripProtocol(s string) string {
2585 return strings.Replace(strings.Replace(s, "https://", "", -1), "http://", "", -1)
2586}
2587func repeat(s string, count int) string {
2588 var result string
2589 for i := 0; i < count; i++ {
2590 result += s
2591 }
2592 return result
2593}
2594func sortsubcats(subcats []string, counts map[string]map[string]int) []string {
2595 sort.Slice(subcats, func(i, j int) bool {
2596 catI, catJ := subcats[i], subcats[j]
2597 countI, countJ := counts[catI]["count"], counts[catJ]["count"]
2598 return countI > countJ
2599 })
2600 return subcats
2601}
2602
2603func subcatlink(subcategory string) string {
2604 s := subcategory
2605 s = strings.ReplaceAll(s, "ΒΌ", "quarter-")
2606 s = strings.ReplaceAll(s, "Β½", "half-")
2607 s = strings.ReplaceAll(s, "1/16", "sixteenth-")
2608 s = strings.ReplaceAll(s, "%", "-pct")
2609 s = strings.ReplaceAll(s, " ", " ")
2610 s = strings.ReplaceAll(s, "watt1", "watt-1")
2611 s = strings.ReplaceAll(s, "watt5", "watt-5")
2612 s = strings.ReplaceAll(s, " ", "-")
2613 s = strings.ReplaceAll(s, "--", "-")
2614 return s
2615}
2616
2617
2618// ===== wasm.go =====
2619// Package main wasm.go
2620package main
2621
2622import (
2623 "fmt"
2624 "log"
2625 "path/filepath"
2626 "strings"
2627 "time"
2628
2629 "github.com/bitfield/script"
2630 "github.com/briandowns/spinner"
2631)
2632
2633func compileWASM() {
2634 s := spinner.New(spinner.CharSets[14], 25*time.Millisecond)
2635 s.Suffix = " Compiling wasm..."
2636 for _, wasmSRC := range f.WasmSRC {
2637 ascend := strings.Repeat("../", len(strings.Split(wasmSRC, "/")))
2638 outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + ".wasm"
2639 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)
2640 log.Println("Compiling wasm with:")
2641 log.Println(compilecmd)
2642 s.Start()
2643 _, err := script.Exec(compilecmd).Stdout()
2644 if err != nil {
2645 log.Fatal(err)
2646 }
2647 s.Stop()
2648 log.Println("Compiled wasm!")
2649 }
2650 for _, wasmSRC := range f.WasmSRC {
2651 ascend := strings.Repeat("../", len(strings.Split(wasmSRC, "/")))
2652 outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + "-tiny.wasm"
2653 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)
2654 log.Println("compiling wasm with:")
2655 log.Println(compilecmd)
2656 s.Start()
2657 _, err := script.Exec(compilecmd).Stdout()
2658 if err != nil {
2659 log.Fatal(err)
2660 }
2661 s.Stop()
2662 log.Println("Compiled wasm!")
2663 }
2664}
2665
2666func ldflags(s string) (ss string) {
2667 checkFiles, err := script.FindFiles(s).Slice()
2668 if err != nil {
2669 log.Fatal(err)
2670 }
2671 if f.LDFlagsX != "" {
2672 for _, s := range checkFiles {
2673 res, err := script.File(s).Match(strings.Split(f.LDFlagsX, "=")[0]).String()
2674 if err != nil {
2675 log.Fatal(err)
2676 }
2677 if res != "" {
2678 ss += fmt.Sprintf(` -X 'main.%s' `, f.LDFlagsX)
2679 break
2680 }
2681 }
2682 }
2683 for _, s := range checkFiles {
2684 res, err := script.File(s).Match("wasmName").String()
2685 if err != nil {
2686 log.Fatal(err)
2687 }
2688 if res != "" {
2689 ss += fmt.Sprintf(` -X 'main.wasmName=%s' `, strings.TrimSuffix(filepath.Base(s), filepath.Ext(s))+".wasm")
2690 break
2691 }
2692 }
2693 if ss != "" {
2694 ss = `-ldflags="` + ss + `"`
2695 }
2696 return ss
2697}
2698
2699