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), " ", "&nbsp;", -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