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