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