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() + "/lib/wasm/wasm_exec.js",                                    //nolint
 119	WasmExecPathGo:     runtime.GOROOT() + "/lib/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
 915
 916func main() { Execute() }
 917
 918var collapseNewlines = regexp.MustCompile(`\n{2,}`)
 919
 920func methodColor(method string, colors fiber.Colors) string {
 921	switch method {
 922	case fiber.MethodGet:
 923		return colors.Cyan
 924	case fiber.MethodPost:
 925		return colors.Green
 926	case fiber.MethodPut:
 927		return colors.Yellow
 928	case fiber.MethodDelete:
 929		return colors.Red
 930	case fiber.MethodPatch:
 931		return colors.White
 932	case fiber.MethodHead:
 933		return colors.Magenta
 934	case fiber.MethodOptions:
 935		return colors.Blue
 936	default:
 937		return colors.Reset
 938	}
 939}
 940
 941func statusColor(code int, colors fiber.Colors) string {
 942	switch {
 943	case code >= fiber.StatusOK && code < fiber.StatusMultipleChoices:
 944		return colors.Green
 945	case code >= fiber.StatusMultipleChoices && code < fiber.StatusBadRequest:
 946		return colors.Blue
 947	case code >= fiber.StatusBadRequest && code < fiber.StatusInternalServerError:
 948		return colors.Yellow
 949	default:
 950		return colors.Red
 951	}
 952}
 953
 954func server() {
 955	wg := new(sync.WaitGroup)
 956	wg.Add(1)
 957	initTMPL()
 958	r := fiber.New(fiber.Config{
 959		ErrorHandler: func(c fiber.Ctx, err error) error {
 960			code := fiber.StatusInternalServerError
 961			var e *fiber.Error
 962			if errors.As(err, &e) {
 963				code = e.Code
 964			}
 965			c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
 966			return c.Status(code).SendString(err.Error())
 967		},
 968	})
 969
 970	r.Use(func(c fiber.Ctx) error {
 971		start := time.Now()
 972		err := c.Next()
 973		status := c.Response().StatusCode()
 974		lat := time.Since(start)
 975		colors := c.App().Config().ColorScheme
 976		ip := fmt.Sprintf("%*s", 15, c.IP())
 977		ipsStr := strings.Join(c.IPs(), ", ")
 978		ips := fmt.Sprintf("%*s", 15, ipsStr)
 979		method := fmt.Sprintf("%-*s", 6, c.Method())
 980		statCol := statusColor(status, colors) + fmt.Sprintf("%3d", status) + colors.Reset
 981		methCol := methodColor(c.Method(), colors) + method + colors.Reset
 982		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())
 983		return err
 984	})
 985	serveSourceCode(r)
 986	serveWASM(r)
 987	r.Get("/logo", logo)
 988	r.Get("/logo/:width", logo)
 989	r.Get("/logo/:width/:height", logo)
 990	r.Get("/logo.png", sendFile)
 991	r.Get("/logo.html", sendFile)
 992	r.Get("/mobilelogo.html", sendFile)
 993	r.Get("/logolarge.html", sendFile)
 994	r.Get("/favicon.ico", sendImage)
 995	r.Get("/robots.txt", robots)
 996	if f.Siteimagesrc == "" {
 997		r.Use("/i", static.New("./img"))
 998		r.Use("/img", static.New("./img"))
 999	}
1000	r.Use("/font", static.New("./font"))
1001	r.Get("/stl/:filename", func(c fiber.Ctx) error { return c.SendFile("./img/stl/" + c.Params("filename")) })
1002	r.Get("/stl/base64/:filename", stlbase64)
1003	r.Get("/site.webmanifest", func(c fiber.Ctx) error {
1004		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"}`))
1005	})
1006	r.Get("/sitemap", sitemap)
1007	r.Get("/sitemap.xml", sitemap)
1008	r.Get("/coffee", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })
1009	r.Get("/clock", clock)
1010	r.Get("/", homepage)
1011	r.Get("/p/:partno", productpage)
1012	r.Get("/post/:partno", handlecat)
1013	r.Get("/p", handlecat)
1014	r.Get("/cat", handlecat)
1015	r.Get("/cat/:cat", handlecat)
1016	r.Get("/cat/:cat/:subcat", handlecat)
1017	r.Get("/style.css", style)
1018	handleOthers(r)
1019	handleOrder(r)
1020	handleCORE(r)
1021	go func() {
1022		err := r.Listen(fmt.Sprintf(":%d", f.WebPort))
1023		if err != nil {
1024			log.Println("Error serving http: ", err)
1025		}
1026		wg.Done()
1027	}()
1028	watchCORE()
1029	compileWASM()
1030	wg.Wait()
1031}
1032
1033func sitemap(c fiber.Ctx) error {
1034	c.Type("xml", "utf-8")
1035	return c.SendString(generateSitemapXML())
1036}
1037
1038func clock(c fiber.Ctx) error {
1039	c.Set("Content-Type", "text/html;charset=utf-8")
1040	_, err := c.Status(fiber.StatusOK).Write([]byte(h.Clock()))
1041	return err
1042}
1043
1044func logo(c fiber.Ctx) error {
1045	tmpl, err := auxTmpl()
1046	if err != nil {
1047		msg := fmt.Sprintf("Error parsing html template: %v", err)
1048		log.Println(msg)
1049		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1050	}
1051	tmpl0, err := tmpl.Clone()
1052	if err != nil {
1053		msg := fmt.Sprintf("Error cloning template: %v", err)
1054		log.Println(msg)
1055		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1056	}
1057	_, err = tmpl0.New("main").Parse(h.Logo())
1058	if err != nil {
1059		msg := fmt.Sprintf("Error parsing product page template: %v", err)
1060		log.Println(msg)
1061		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1062	}
1063	tmpl = tmpl0
1064	c.Set("Content-Type", "text/html;charset=utf-8")
1065
1066	img2txtFlags := ""
1067	if w, err := strconv.Atoi(c.Params("width")); err == nil {
1068		img2txtFlags = fmt.Sprintf("--width=%d ",w)
1069	}
1070	if h, err := strconv.Atoi(c.Params("height")); err == nil {
1071		img2txtFlags = fmt.Sprintf("--height=%d ",h)
1072	}
1073
1074	logoHTMLslice, err := script.Exec(fmt.Sprintf("bash -c 'img2txt %s logo.jpg | ansifilter -H'", img2txtFlags)).Slice()
1075	if err != nil {
1076		log.Println("error: ", err)
1077		_, err = c.Status(fiber.StatusInternalServerError).Write([]byte(err.Error()+"/n"+strings.Join(logoHTMLslice,"\n")))
1078		return err
1079	}
1080	if len(logoHTMLslice) > 2 {
1081	    logoHTMLslice = logoHTMLslice[:len(logoHTMLslice)-3]
1082	}
1083	if len(logoHTMLslice) > 18 {
1084	    logoHTMLslice = logoHTMLslice[19:]
1085	}
1086
1087
1088	var result bytes.Buffer
1089	h1 := pageMeta(c, htmlTemplateData{})
1090	h1.Page = "logo"
1091	h1.Title = "logo"
1092	tmplData := map[string]interface{}{
1093		"Content": strings.Join(logoHTMLslice, "\n"),
1094	}
1095	err = tmpl.Execute(&result, tmplData)
1096	if err != nil {
1097		log.Println("error: ", err)
1098		_, err = c.Status(fiber.StatusInternalServerError).Write(result.Bytes())
1099		return err
1100	}
1101	_, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1102	return err
1103}
1104
1105func robots(c fiber.Ctx) error {
1106	c.Set("Content-Type", "text/plain;charset=utf-8")
1107	_, err := c.Status(fiber.StatusOK).Write([]byte(fmt.Sprintf("User-agent: *\n\nSitemap: https://%s/sitemap.xml", c.Hostname())))
1108	return err
1109}
1110
1111func style(c fiber.Ctx) error {
1112	c.Set("Content-Type", "text/css;charset=utf-8")
1113	_, err := c.Status(fiber.StatusOK).Write([]byte(h.StyleCSS()))
1114	return err
1115}
1116
1117func serveWASM(r *fiber.App) {
1118	if f.WasmExecPath != "" {
1119		_, err := script.File(f.WasmExecPath).Bytes()
1120		if err != nil {
1121			log.Printf("Error reading %s: %v\n", f.WasmExecPath, err)
1122		} else { //the wasm exec must be present or none of the webassembly stuff will work ; provided by the golang installaton
1123			r.Get(f.WasmExecPathTinyGo, func(c fiber.Ctx) error {
1124				wasmExecData, err := script.File(f.WasmExecPathTinyGo).Bytes()
1125				if err != nil {
1126					log.Printf("Error reading %s: %v\n", f.WasmExecPathTinyGo, err)
1127					return c.SendStatus(fiber.StatusNotFound)
1128				}
1129				c.Set("Content-Type", "application/js")
1130				_, err = c.Status(fiber.StatusOK).Write(wasmExecData)
1131				return err
1132			})
1133
1134			r.Get(f.WasmExecPathGo, func(c fiber.Ctx) error {
1135				wasmExecData, err := script.File(f.WasmExecPathGo).Bytes()
1136				if err != nil {
1137					log.Printf("Error reading %s: %v\n", f.WasmExecPathGo, err)
1138					return c.SendStatus(fiber.StatusNotFound)
1139				}
1140				c.Set("Content-Type", "application/js")
1141				_, err = c.Status(fiber.StatusOK).Write(wasmExecData)
1142				return err
1143			})
1144
1145			suffix := ".wasm"
1146			if f.UseTinygo {
1147				suffix = "-tiny.wasm"
1148			}
1149			for _, wasmSRC := range f.WasmSRC {
1150				outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + suffix
1151				r.Get("/"+outputFile, func(c fiber.Ctx) error {
1152					data, err := script.File(outputFile).Bytes()
1153					if err != nil {
1154						script.File(outputFile).Stdout() //nolint
1155						return c.SendStatus(fiber.StatusInternalServerError)
1156					}
1157					c.Set("Content-Type", "application/wasm")
1158					return c.Status(fiber.StatusOK).Send(data)
1159				})
1160			}
1161		}
1162	}
1163}
1164
1165func sendFile(c fiber.Ctx) error {
1166	return c.SendFile("." + c.Path())
1167}
1168func sendImage(c fiber.Ctx) error {
1169	c.Set("Content-Type", "image/jpeg")
1170	return c.SendFile("./img" + c.Path())
1171}
1172
1173func stlbase64(c fiber.Ctx) error {
1174	stlfile, err := script.File("img/stl/" + c.Params("filename")).Bytes()
1175	if err != nil {
1176		_, _ = script.File("img/stl/" + c.Params("filename")).Stdout() //nolint
1177		return c.SendStatus(fiber.StatusNotFound)
1178	}
1179	_, err = c.Status(fiber.StatusOK).Write([]byte("data:model/stl;base64," + base64.StdEncoding.EncodeToString(stlfile)))
1180	return err
1181}
1182
1183type item struct {
1184	ID     string
1185	Amount int64
1186}
1187
1188func cathtmlfunc(c fiber.Ctx) error {
1189	tmpl, err := mainTmpl()
1190	if err != nil {
1191		msg := fmt.Sprintf("Error parsing html template: %v", err)
1192		log.Println(msg)
1193		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1194	}
1195	tmpl0, err := tmpl.Clone()
1196	if err != nil {
1197		msg := fmt.Sprintf("Error cloning html template: %v", err)
1198		log.Println(msg)
1199		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1200	}
1201	_, err = tmpl0.New("main").Parse(h.CategoryPage())
1202	if err != nil {
1203		msg := fmt.Sprintf("Error parsing Category page template: %v", err)
1204		log.Println(msg)
1205		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1206	}
1207	tmpl = tmpl0
1208	var tmplData map[string]interface{}
1209	var result bytes.Buffer
1210	var categoryproducts p.Products
1211	c.Set("Content-Type", "text/html;charset=utf-8")
1212	h1 := pageMeta(c, htmlPageTemplateData)
1213	h1.Title = fmt.Sprintf("%s | %s", func() string {
1214		var str string
1215		if c.Params("partno") != "" {
1216			return "No product matching partno.: " + c.Params("partno") + " | Showing All Products"
1217		}
1218		if c.Params("cat") == "" {
1219			return "All Products"
1220		}
1221		str = fmt.Sprintf("Category: %s", c.Params("cat"))
1222		if c.Params("subcat") != "" {
1223			str += fmt.Sprintf("; Subcategory: %s", c.Params("subcat"))
1224		}
1225		return str
1226	}(), h1.Title)
1227	h1.Page = "category"
1228	if c.Params("cat") == "" && c.Params("subcat") == "" {
1229		tmplData = map[string]interface{}{
1230			"Products":    allproducts,
1231			"Page":        h1,
1232			"Category":    c.Params("cat"),
1233			"Subcategory": c.Params("subcat"),
1234			"Prods":       allproducts,
1235			"Product":     c.Params("partno"),
1236		}
1237	} else {
1238
1239		for _, prod := range allproducts {
1240			if strings.EqualFold(prod.Category, c.Params("cat")) && (c.Params("subcat") == "" || strings.EqualFold(escapesubcat(prod.Subcategory), c.Params("subcat"))) {
1241				categoryproducts = append(categoryproducts, prod)
1242			}
1243		}
1244		tmplData = map[string]interface{}{
1245			"Products":    categoryproducts,
1246			"Page":        h1,
1247			"Category":    c.Params("cat"),
1248			"Subcategory": c.Params("subcat"),
1249			"Prods":       allproducts,
1250		}
1251	}
1252	err = tmpl.Execute(&result, tmplData)
1253	if err != nil {
1254		msg := fmt.Sprintf("Error execute html template: %v", err)
1255		log.Println(msg)
1256		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1257	}
1258	_, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1259	return err
1260}
1261
1262func getcats() (cats []string) {
1263	var catsMap = make(map[string]int)
1264	for _, prod := range allproducts {
1265		catsMap[prod.Category]++
1266	}
1267	for cat := range catsMap {
1268		cats = append(cats, cat)
1269	}
1270	return cats
1271}
1272func contains(slice []string, str string) bool {
1273	for _, s := range slice {
1274		if s == str {
1275			return true
1276		}
1277	}
1278	return false
1279}
1280func getcategories(allproducts p.Products) (map[string]int, []string, map[string]map[string]int, map[string][]string) {
1281	categoryCounts := make(map[string]int)
1282	subcategoryCounts := make(map[string]map[string]int)
1283	subcategoriesByCategory := make(map[string][]string)
1284
1285	for _, prod := range allproducts {
1286		if prod.Category != "" {
1287			categoryCounts[prod.Category]++
1288			if prod.Subcategory != "" {
1289				if subcategoryCounts[prod.Category] == nil {
1290					subcategoryCounts[prod.Category] = make(map[string]int)
1291				}
1292				subcategoryCounts[prod.Category][prod.Subcategory]++
1293				if !contains(subcategoriesByCategory[prod.Category], prod.Subcategory) {
1294					subcategoriesByCategory[prod.Category] = append(subcategoriesByCategory[prod.Category], prod.Subcategory)
1295				}
1296			}
1297		}
1298	}
1299
1300	var sortableCategories []struct {
1301		Name  string
1302		Count int
1303	}
1304	for cat, count := range categoryCounts {
1305		sortableCategories = append(sortableCategories, struct {
1306			Name  string
1307			Count int
1308		}{Name: cat, Count: count})
1309	}
1310	sort.Slice(sortableCategories, func(i, j int) bool {
1311		return sortableCategories[i].Count > sortableCategories[j].Count
1312	})
1313	var sortedCategories []string
1314	for _, cat := range sortableCategories {
1315		sortedCategories = append(sortedCategories, cat.Name)
1316		var sortableSubcategories []struct {
1317			Name  string
1318			Count int
1319		}
1320		for subcat, count := range subcategoryCounts[cat.Name] {
1321			sortableSubcategories = append(sortableSubcategories, struct {
1322				Name  string
1323				Count int
1324			}{Name: subcat, Count: count})
1325		}
1326		sort.Slice(sortableSubcategories, func(i, j int) bool {
1327			return sortableSubcategories[i].Count > sortableSubcategories[j].Count
1328		})
1329		var sortedSubcategories []string
1330		for _, subcat := range sortableSubcategories {
1331			sortedSubcategories = append(sortedSubcategories, subcat.Name)
1332		}
1333		subcategoriesByCategory[cat.Name] = sortedSubcategories
1334	}
1335	return categoryCounts, sortedCategories, subcategoryCounts, subcategoriesByCategory
1336}
1337
1338func getsubcats(cat string) (subcats []string) {
1339	var subcatsMap = make(map[string]int)
1340	for _, prod := range allproducts {
1341		if cat == "" || strings.EqualFold(cat, prod.Category) {
1342			if prod.Subcategory != "" {
1343				subcatsMap[escapesubcat(prod.Subcategory)]++
1344			}
1345		}
1346	}
1347	for subcat := range subcatsMap {
1348		subcats = append(subcats, subcat)
1349	}
1350	return subcats
1351}
1352func escapesubcat(sc string) (esc string) {
1353	esc = strings.Replace(sc, "ΒΌ", "quarter-", -1)
1354	esc = strings.Replace(esc, "Β½", "half-", -1)
1355	esc = strings.Replace(esc, "1/16", "sixteenth-", -1)
1356	esc = strings.Replace(esc, "%", "-pct", -1)
1357	esc = strings.Replace(esc, "  ", " ", -1)
1358	esc = strings.Replace(esc, " ", "-", -1)
1359	esc = strings.Replace(esc, "--", "-", -1)
1360	esc = strings.Replace(esc, "watt1", "watt-1", -1)
1361	esc = strings.Replace(esc, "watt5", "watt-5", -1)
1362	return esc
1363}
1364
1365func handlecat(c fiber.Ctx) error {
1366	if c.Params("cat") == "" && c.Params("subcat") == "" {
1367		return cathtmlfunc(c)
1368	}
1369	var catexists bool
1370	var subcatexists bool
1371	catexists = false
1372	for _, cat := range getcats() {
1373		if strings.EqualFold(cat, c.Params("cat")) {
1374			catexists = true
1375			break
1376		}
1377	}
1378	subcatexists = false
1379	if c.Params("subcat") != "" {
1380		for _, subcat := range getsubcats("") {
1381			if strings.EqualFold(escapesubcat(subcat), c.Params("subcat")) {
1382				subcatexists = true
1383				break
1384			}
1385		}
1386	}
1387	if c.Params("subcat") != "" && !subcatexists {
1388		log.Printf("subcategory %s does not match any existing subcategory\n", c.Params("subcat"))
1389		return c.Redirect().To("/cat/" + c.Params("cat"))
1390	}
1391	if !catexists {
1392		log.Printf("category %s does not match any existing category\n", c.Params("cat"))
1393		return c.Redirect().To("/cat")
1394	}
1395	if catexists || (catexists && subcatexists) {
1396		return cathtmlfunc(c)
1397	}
1398	return c.SendStatus(fiber.StatusNotFound)
1399}
1400
1401func homepage(c fiber.Ctx) error {
1402	tmpl, err := mainTmpl()
1403	if err != nil {
1404		msg := fmt.Sprintf("Could not parsing html template: %v", err)
1405		log.Println(msg)
1406		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1407	}
1408	tmpl0, err := tmpl.Clone()
1409	if err != nil {
1410		msg := fmt.Sprintf("Error cloning template: %v", err)
1411		log.Println(msg)
1412		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1413	}
1414	_, err = tmpl0.New("main").Parse(h.FrontPage())
1415	if err != nil {
1416		msg := fmt.Sprintf("Error parsing Front Page template: %v", err)
1417		log.Println(msg)
1418		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1419	}
1420	_, err = tmpl0.New("about").Parse(h.AboutPage())
1421	if err != nil {
1422		msg := fmt.Sprintf("Error parsing About Page template: %v", err)
1423		log.Println(msg)
1424		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1425	}
1426	_, err = tmpl0.New("policy").Parse(h.PolicyPage())
1427	if err != nil {
1428		msg := fmt.Sprintf("Error parsing Policy Page template: %v", err)
1429		log.Println(msg)
1430		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1431	}
1432	_, err = tmpl0.New("links").Parse(h.LinksPage())
1433	if err != nil {
1434		msg := fmt.Sprintf("Error parsing Links Page template: %v", err)
1435		log.Println(msg)
1436		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1437	}
1438	tmpl = tmpl0
1439	log.Println(c.Get("User-Agent"))
1440	c.Set("Content-Type", "text/html;charset=utf-8")
1441	h1 := pageMeta(c, htmlPageTemplateData)
1442	tmplData := map[string]interface{}{
1443		"Page":  h1,
1444		"Prods": allproducts,
1445	}
1446	var result bytes.Buffer
1447	err = tmpl.Execute(&result, tmplData)
1448	if err != nil {
1449		msg := fmt.Sprintf("Error executing template: %v", err)
1450		log.Println(msg)
1451		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1452	}
1453	_, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1454	return err
1455}
1456
1457func productpage(c fiber.Ctx) error {
1458	tmpl, err := mainTmpl()
1459	if err != nil {
1460		msg := fmt.Sprintf("Error parsing html template: %v", err)
1461		log.Println(msg)
1462		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1463	}
1464	tmpl0, err := tmpl.Clone()
1465	if err != nil {
1466		msg := fmt.Sprintf("Error cloning template: %v", err)
1467		log.Println(msg)
1468		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1469	}
1470	_, err = tmpl0.New("main").Parse(h.ProductPage())
1471	if err != nil {
1472		msg := fmt.Sprintf("Error parsing product page template: %v", err)
1473		log.Println(msg)
1474		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1475	}
1476	tmpl = tmpl0
1477	c.Set("Content-Type", "text/html;charset=utf-8")
1478	for _, prod := range allproducts {
1479		if strings.EqualFold(prod.Partno, c.Params("partno")) {
1480			var result bytes.Buffer
1481			h1 := pageMeta(c, htmlPageTemplateData)
1482			h1.Page = "product"
1483			h1.Title = fmt.Sprintf("%s | %s", prod.Name, h1.Title)
1484			tmplData := map[string]interface{}{
1485				"Prod":  prod,
1486				"Page":  h1,
1487				"Prods": allproducts,
1488			}
1489			err := tmpl.Execute(&result, tmplData)
1490			if err != nil {
1491				log.Println("error: ", err)
1492				_, err = c.Status(fiber.StatusInternalServerError).Write(result.Bytes())
1493				return err
1494			}
1495			_, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1496			return err
1497		}
1498	}
1499	log.Printf("product %s does not match any existing product\n", c.Params("partno"))
1500	return c.Status(fiber.StatusNotFound).Redirect().To("/cat")
1501}
1502
1503
1504// ===== order.go =====
1505// Package main stripe.go
1506package main
1507
1508import (
1509	"bytes"
1510	"encoding/json"
1511	"fmt"
1512	htmpl "html/template"
1513	"log"
1514	"os"
1515	"path/filepath"
1516	"strings"
1517	"time"
1518
1519	"github.com/bitfield/script"
1520	"github.com/gofiber/fiber/v3"
1521	"github.com/stripe/stripe-go/v81"
1522	"github.com/stripe/stripe-go/v81/paymentintent"
1523)
1524
1525func handleOrder(r *fiber.App) {
1526	r.Get("/checkout.css", func(c fiber.Ctx) error {
1527		c.Set("Content-Type", "text/css;charset=utf-8")
1528		_, err := c.Status(fiber.StatusOK).Write([]byte(h.CheckoutCSS()))
1529		return err
1530	})
1531
1532	r.Get("/complete", func(c fiber.Ctx) error {
1533		// Complete template
1534		completetmpl := htmpl.New("index")
1535		if _, err := completetmpl.Parse(h.CompletePage()); err != nil {
1536			msg := fmt.Sprintf("Error parsing complete page template: %v", err)
1537			log.Println(msg)
1538			return c.Status(fiber.StatusInternalServerError).SendString(msg)
1539		}
1540		if _, err := completetmpl.New("wasm").Parse(h.Wasm()); err != nil {
1541			log.Println("Error parsing wasm template:", err)
1542			msg := fmt.Sprintf("Error parsing wasm template: %v", err)
1543			log.Println(msg)
1544			return c.Status(fiber.StatusInternalServerError).SendString(msg)
1545		}
1546		h1 := htmlPageTemplateData
1547		/*
1548			proto := "http"
1549			if c.Secure() {
1550				proto += "s"
1551			}
1552		*/
1553		proto := "https"
1554		h1.Canonical = proto + `://` + c.Hostname() + c.OriginalURL()
1555		h1.BaseURL = proto + `://` + c.Hostname()
1556		h1.RequestHost = c.Hostname()
1557		h1.Protocol = proto
1558		h1.Time = time.Now().Format(time.RFC3339Nano)
1559		h1.Year = fmt.Sprintf("%v", time.Now().Year())
1560		tmplData := map[string]interface{}{
1561			"Page": h1,
1562		}
1563		var result bytes.Buffer
1564		err := completetmpl.Execute(&result, tmplData)
1565		if err != nil {
1566			msg := fmt.Sprintf("Could not execute html template %v", err)
1567			log.Println(msg)
1568			return c.Status(fiber.StatusInternalServerError).SendString(msg)
1569		}
1570		c.Set("Content-Type", "text/html;charset=utf-8")
1571		return c.Status(fiber.StatusOK).Send(result.Bytes())
1572	})
1573
1574	r.Get("/order/:piid", func(c fiber.Ctx) error {
1575		piid := c.Params("piid")
1576		order, err := script.File("orders/" + piid + ".json").Bytes()
1577		if err != nil {
1578			return c.Status(fiber.StatusNotFound).SendString("Order not found")
1579		}
1580		return c.Status(fiber.StatusOK).Send(order)
1581	})
1582
1583	r.Get("/order/:piid/html", func(c fiber.Ctx) error {
1584		piid := c.Params("piid")
1585		order, err := script.File("orders/" + piid + ".json").Bytes()
1586		if err != nil {
1587			return c.Status(fiber.StatusNotFound).SendString("Order not found")
1588		}
1589		var m map[string]interface{}
1590		if err := json.Unmarshal(order, &m); err != nil { return c.Status(500).SendString("failed to unmarshal order json: "+err.Error()) }
1591		receipt, err := buildReceipt(m, piid)
1592		if err != nil { return c.Status(500).SendString("failed to build reciept: "+err.Error()) }
1593		return c.Status(200).SendString(string(receipt))
1594	})
1595
1596	r.Post("/create-payment-intent", func(c fiber.Ctx) error {
1597		rawBody := c.Body()
1598		if rawBody == nil {
1599			log.Printf("Failed to read raw request body")
1600			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to read request body"})
1601		}
1602		log.Printf("Raw request body: %s", string(rawBody))
1603
1604		var req struct {
1605			Items []item `json:"items"`
1606		}
1607		if err := json.Unmarshal(rawBody, &req); err != nil {
1608			log.Printf("Failed to parse JSON: %v", err)
1609			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
1610		}
1611
1612		total := int64(0)
1613		for _, item := range req.Items {
1614			total += item.Amount
1615		}
1616
1617		params := &stripe.PaymentIntentParams{
1618			Amount:   stripe.Int64(total),
1619			Currency: stripe.String(string(stripe.CurrencyUSD)),
1620		}
1621		pi, err := paymentintent.New(params)
1622		if err != nil {
1623			log.Printf("Failed to create PaymentIntent: %v", err)
1624			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
1625		}
1626
1627		log.Printf("Created PaymentIntent with ClientSecret: %v", pi.ClientSecret)
1628		return c.Status(fiber.StatusOK).JSON(fiber.Map{
1629			"clientSecret":   pi.ClientSecret,
1630			"dpmCheckerLink": fmt.Sprintf("https://dashboard.stripe.com/settings/payment_methods/review?transaction_id=%s", pi.ID),
1631		})
1632	})
1633
1634	r.Post("/submit-order", func(c fiber.Ctx) error {
1635		var requestData struct {
1636			LocalStorageData map[string]interface{} `json:"localStorageData"`
1637			PaymentIntentID  string                 `json:"paymentIntentId"`
1638		}
1639
1640		if err := c.Bind().Body(&requestData); err != nil {
1641			log.Println(err)
1642			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request data"})
1643		}
1644
1645		log.Printf("Received order data: %+v\n", requestData.LocalStorageData)
1646		log.Printf("Received payment intent ID: %s\n", requestData.PaymentIntentID)
1647
1648		paymentIntent, err := paymentintent.Get(requestData.PaymentIntentID, nil)
1649		if err != nil {
1650			log.Printf("Error retrieving payment intent: %v", err)
1651			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to verify payment"})
1652		}
1653		if paymentIntent.Status != stripe.PaymentIntentStatusSucceeded {
1654			log.Printf("Payment was not successful, status: %s", paymentIntent.Status)
1655			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Payment not successful"})
1656		}
1657
1658		ordersDir := "./orders"
1659		if err := os.MkdirAll(ordersDir, os.ModePerm); err != nil {
1660			log.Printf("Error creating orders directory: %v", err)
1661			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1662		}
1663
1664		filePath := filepath.Join(ordersDir, fmt.Sprintf("%s.json", requestData.PaymentIntentID))
1665		data, err := json.MarshalIndent(requestData.LocalStorageData, "", "  ")
1666		if err != nil {
1667			log.Printf("Error marshaling data to json: %v", err)
1668			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1669		}
1670		if err := os.WriteFile(filePath, data, 0o644); err != nil {
1671			log.Printf("Error writing data to file: %v", err)
1672			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1673		}
1674
1675		// ---- Print receipt via CUPS (non-blocking so your response is snappy)
1676		go func(pid string, local map[string]interface{}) {
1677			receipt, err := buildReceipt(local, pid)
1678			if err != nil {
1679				log.Printf("build receipt failed: %v", err)
1680				return
1681			}
1682			if err := sendToCUPS(receipt, "Order "+pid); err != nil {
1683				log.Printf("print failed: %v", err)
1684				// optional: mark a sentinel file so you can reprint later
1685				_ = os.WriteFile(filepath.Join(ordersDir, pid+".print_failed"), []byte(err.Error()), 0o644)
1686			}
1687		}(requestData.PaymentIntentID, requestData.LocalStorageData)
1688
1689		return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Order submitted successfully"})
1690	})
1691
1692	/*
1693			r.Post("/reprint/:pid", func(c fiber.Ctx) error {
1694		    pid := c.Params("pid")
1695		    b, err := os.ReadFile(filepath.Join("./orders", pid+".json"))
1696		    if err != nil { return c.Status(404).SendString("not found") }
1697		    var m map[string]interface{}
1698		    if err := json.Unmarshal(b, &m); err != nil { return c.Status(500).SendString(err.Error()) }
1699		    receipt, err := buildReceipt(m, pid)
1700		    if err != nil { return c.Status(500).SendString(err.Error()) }
1701		    if err := sendToCUPS(receipt, "Order "+pid); err != nil {
1702		        return c.Status(500).SendString(err.Error())
1703		    }
1704		    return c.SendStatus(204)
1705		})
1706	*/
1707}
1708
1709func buildReceipt(local map[string]interface{}, paymentIntentID string) ([]byte, error) {
1710	// Pretty JSON body from what you already persisted
1711	body, err := json.MarshalIndent(local, "", "  ")
1712	if err != nil {
1713		return nil, err
1714	}
1715	// Simple text receipt header
1716	ts := time.Now().Format("2006-01-02 15:04:05")
1717	hdr := fmt.Sprintf(
1718		"==================== ORDER ====================\n"+
1719			"PaymentIntent: %s\nTime: %s\n===============================================\n\n",
1720		paymentIntentID, ts,
1721	)
1722	// Footer (optional)
1723	ftr := "\n\n---------------------- END ---------------------\n"
1724	receipt := append([]byte(hdr), body...)
1725	receipt = append(receipt, []byte(ftr)...)
1726	return receipt, nil
1727}
1728
1729// escape for inclusion inside *double quotes* in a bash command string
1730func bashEscapeDoubleQuoted(s string) string {
1731	s = strings.ReplaceAll(s, `\`, `\\`)
1732	s = strings.ReplaceAll(s, `"`, `\"`)
1733	s = strings.ReplaceAll(s, "$", `\$`)
1734	s = strings.ReplaceAll(s, "`", "\\`")
1735	return s
1736}
1737
1738func sendToCUPS(receipt []byte, title string) error {
1739	if title == "" {
1740		title = "Order"
1741	}
1742	var cmd strings.Builder
1743	cmd.WriteString("lp")
1744
1745	if f.PrinterName != "" {
1746		cmd.WriteString(` -d "`)
1747		cmd.WriteString(bashEscapeDoubleQuoted(f.PrinterName))
1748		cmd.WriteString(`"`)
1749	}
1750
1751	cmd.WriteString(` -t "`)
1752	cmd.WriteString(bashEscapeDoubleQuoted(title))
1753	cmd.WriteString(`"`)
1754
1755	if f.CupsOptions != "" {
1756		for _, opt := range strings.Split(f.CupsOptions, ",") {
1757			opt = strings.TrimSpace(opt)
1758			if opt == "" {
1759				continue
1760			}
1761			cmd.WriteString(` -o "`)
1762			cmd.WriteString(bashEscapeDoubleQuoted(opt))
1763			cmd.WriteString(`"`)
1764		}
1765	}
1766
1767	full := fmt.Sprintf(`bash -lc %q`, cmd.String())
1768
1769	_, err := script.Echo(string(receipt)).Exec(full).Stdout()
1770	if err != nil {
1771		return fmt.Errorf("lp failed: %v", err)
1772	}
1773	return nil
1774}
1775
1776
1777// ===== other.go =====
1778// Package main other.go
1779package main
1780
1781import (
1782	"bytes"
1783	"fmt"
1784	"log"
1785
1786	"github.com/gofiber/fiber/v3"
1787)
1788
1789func handleOthers(r *fiber.App) {
1790	r.Get("/COVID", func(c fiber.Ctx) error {
1791		tmpl, err := mainTmpl()
1792		if err != nil {
1793			msg := fmt.Sprintf("Error parse html template: %v", err)
1794			log.Println(msg)
1795			return c.Status(fiber.StatusInternalServerError).SendString(msg)
1796		}
1797		tmpl0, err := tmpl.Clone()
1798		if err != nil {
1799			msg := fmt.Sprintf("Error cloning template: %v", err)
1800			log.Println(msg)
1801			return c.Status(fiber.StatusInternalServerError).SendString(msg)
1802		}
1803		_, err = tmpl0.New("main").Parse(h.COVIDPage())
1804		if err != nil {
1805			msg := fmt.Sprintf("Error parsing main template: %v", err)
1806			log.Println(msg)
1807			return c.Status(fiber.StatusInternalServerError).SendString(msg)
1808		}
1809		tmpl = tmpl0
1810		log.Println(c.Get("User-Agent"))
1811		c.Set("Content-Type", "text/html;charset=utf-8")
1812		h1 := pageMeta(c, htmlPageTemplateData)
1813		h1.Page = "hidden"
1814		h1.MetaDesc = "The COVID  ΜΆvΜΆaΜΆcΜΆcΜΆiΜΆnΜΆeΜΆ bioweapon injection genocide and the new dark age of humanity"
1815		//		h1.Mobile = strings.Contains(strings.ToLower(c.Get("User-Agent")), "mobile")
1816		tmplData := map[string]interface{}{
1817			"Page":  h1,
1818			"Prods": allproducts,
1819		}
1820		var result bytes.Buffer
1821		err = tmpl.Execute(&result, tmplData)
1822		if err != nil {
1823			msg := fmt.Sprintf("Error executing template: %v", err)
1824			log.Println(msg)
1825			return c.Status(fiber.StatusInternalServerError).SendString(msg)
1826		}
1827		_, 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))
1828		return err
1829	})
1830
1831}
1832
1833
1834// ===== source.go =====
1835// Package main source.go
1836package main
1837
1838import (
1839	"bytes"
1840	"embed"
1841	"fmt"
1842	"io/fs"
1843	"os"
1844	"strings"
1845
1846	"github.com/alecthomas/chroma/v2"
1847	"github.com/alecthomas/chroma/v2/formatters/html"
1848	"github.com/alecthomas/chroma/v2/lexers"
1849	"github.com/alecthomas/chroma/v2/styles"
1850	"github.com/gofiber/fiber/v3"
1851)
1852
1853//go:embed *.go
1854var quine embed.FS
1855
1856var sourceWasm = os.DirFS("wasm")
1857var sourcesWasm []fs.FS
1858var sourceCore = os.DirFS("ui")
1859var sourceHtml = os.DirFS("htmpl")
1860var sourceContent = os.DirFS("content")
1861
1862func serveSourceCode(r *fiber.App) {
1863	for _, wasmSRC := range f.WasmSRC {
1864		sourcesWasm = append(sourcesWasm, os.DirFS(wasmSRC))
1865	}
1866	r.Get("/sourcecode", func(c fiber.Ctx) error {
1867		ret := `<!doctype html>
1868<html lang='en'>
1869<head>
1870<link rel="stylesheet" href="/style.css" type="text/css">
1871</head>
1872<body class='grid-container' style='background-color:black;color:white;'>
1873<a href='/sourcecode/go'>GO</a><br><br>
1874
1875<a href='/sourcecode/html'>HTML</a><br><br>
1876
1877<a href='/sourcecode/content'>Content</a><br><br>
1878
1879<a href='/sourcecode/core'>C.O.R.E.</a><br><br>
1880
1881<a href='/sourcecode/wasm'>WASM</a><br><br>
1882
1883</body>
1884</html>
1885`
1886		c.Set("Content-Type", "text/html;charset=utf-8")
1887		_, err := c.Status(fiber.StatusOK).Write([]byte(ret))
1888		return err
1889	})
1890
1891	r.Get("/sourcecode/html", sourcecodehtml)
1892	r.Get("/sourcecode/content", sourcecodecontent)
1893	r.Get("/sourcecode/go", sourcecodego)
1894	r.Get("/sourcecode/core", sourcecodecore)
1895	//	r.Get("/sourcecodewasm", sourcecodewasm)
1896	r.Get("/sourcecode/wasm", func(c fiber.Ctx) error {
1897		ret := `<!doctype html>
1898<html lang='en'>
1899<head>
1900<link rel="stylesheet" href="/style.css" type="text/css">
1901</head>
1902<body class='grid-container' style='background-color:black;color:white;'>
1903`
1904		for _, wasmSRC := range f.WasmSRC {
1905			pathNameSlc := strings.Split(wasmSRC, "/")
1906			pathName := pathNameSlc[len(pathNameSlc)-1]
1907			ret += `<a href='/sourcecode/wasm/` + pathName + `'>` + pathName + `</a><br>
1908			`
1909		}
1910		ret += `</body></html>
1911		`
1912		c.Set("Content-Type", "text/html;charset=utf-8")
1913		_, err := c.Status(fiber.StatusOK).Write([]byte(ret))
1914		return err
1915	})
1916
1917	for i, wasmSRC := range f.WasmSRC {
1918		pathNameSlc := strings.Split(wasmSRC, "/")
1919		pathName := pathNameSlc[len(pathNameSlc)-1]
1920		r.Get("/sourcecode/wasm/"+pathName, func(c fiber.Ctx) error {
1921			return sourcecode(c, sourcesWasm[i], "dracula", "go")
1922		})
1923	}
1924}
1925
1926func sourcecodehtml(c fiber.Ctx) error {
1927	return sourcecode(c, sourceHtml, "monokai", "html")
1928}
1929func sourcecodecontent(c fiber.Ctx) error {
1930	return sourcecode(c, sourceContent, "monokai", "html")
1931}
1932func sourcecodego(c fiber.Ctx) error {
1933	return sourcecode(c, quine, "monokai", "go")
1934}
1935
1936func sourcecodewasm(c fiber.Ctx) error {
1937	return sourcecode(c, sourceWasm, "dracula", "go")
1938}
1939
1940func sourcecodecore(c fiber.Ctx) error {
1941	return sourcecode(c, sourceCore, "solarized-dark256", "go")
1942}
1943
1944func sourcecode(c fiber.Ctx, fsys fs.FS, styleName string, lang string) error {
1945	c.Set("Content-Type", "text/html;charset=utf-8")
1946	var buf bytes.Buffer
1947	var builder strings.Builder
1948
1949	fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
1950		if err != nil {
1951			return err
1952		}
1953		if !d.IsDir() && strings.HasSuffix(path, "."+lang) {
1954			content, err := fs.ReadFile(fsys, path)
1955			if err != nil {
1956				return err
1957			}
1958			builder.WriteString(fmt.Sprintf("// ===== %s =====\n", path))
1959			builder.Write(content)
1960			builder.WriteString("\n\n")
1961		}
1962		return nil
1963	})
1964
1965	// Pick lexer & style
1966	lexer := lexers.Get(lang)
1967	if lexer == nil {
1968		lexer = lexers.Fallback
1969	}
1970	lexer = chroma.Coalesce(lexer)
1971
1972	style := styles.Get(styleName)
1973	if style == nil {
1974		style = styles.Fallback
1975	}
1976
1977	// Formatter with line numbers & CSS classes
1978	formatter := html.New(
1979		html.WithLineNumbers(true),
1980		html.WithClasses(true),
1981	)
1982
1983	iterator, err := lexer.Tokenise(nil, builder.String())
1984	if err != nil {
1985		return err
1986	}
1987
1988	// Optional: include CSS in output
1989	var css bytes.Buffer
1990	_ = formatter.WriteCSS(&css, style)
1991	buf.WriteString("<style>")
1992	buf.Write(css.Bytes())
1993	buf.WriteString("</style>")
1994
1995	if err := formatter.Format(&buf, style, iterator); err != nil {
1996		return err
1997	}
1998
1999	_, err = c.Status(fiber.StatusOK).Write(buf.Bytes())
2000	return err
2001}
2002
2003
2004// ===== tmpl.go =====
2005// Package main tmpl.go
2006package main
2007
2008import (
2009	"bytes"
2010	"fmt"
2011	htmpl "html/template"
2012	"log"
2013	"os"
2014	"path/filepath"
2015	"sort"
2016	"strconv"
2017	"strings"
2018	ttmpl "text/template"
2019	"time"
2020
2021	p "github.com/0magnet/m2/pkg/product"
2022	"github.com/gofiber/fiber/v3"
2023)
2024
2025/*
2026//go:embed htmpl/*
2027var templatesFS embed.FS
2028
2029//go:embed content/*
2030var contentFS embed.FS
2031*/
2032/*
2033var (
2034	templatesFS = os.DirFS("htmpl")
2035	contentFS   = os.DirFS("content")
2036)
2037*/
2038/*
2039func mustReadEmbeddedFileToString(path string, fs embed.FS) string {
2040	return string(mustReadEmbeddedFileToBytes(path, fs))
2041}
2042
2043func mustReadEmbeddedFileToBytes(path string, fs embed.FS) []byte {
2044	data, err := fs.ReadFile(path)
2045	if err != nil {
2046		panic(err)
2047	}
2048	return data
2049}
2050*/
2051
2052func mustReadFileToString(path string) string {
2053	return string(mustReadFileToBytes(path))
2054}
2055
2056func mustReadFileToBytes(path string) []byte {
2057	data, err := os.ReadFile(path) //nolint
2058	if err != nil {
2059		panic(err)
2060	}
2061	return data
2062}
2063
2064type htmlTemplate struct {
2065	Empty           func() string
2066	Head           func() string
2067	Logo           func() string
2068	Header         func() string
2069	Categories     func() string
2070	CatSubcats     func() string
2071	Footer         func() string
2072	MainPage       func() string
2073	AuxPage        func() string
2074	FrontPage      func() string
2075	CategoryPage   func() string
2076	CategoryPageMD func() string
2077	ProductPage    func() string
2078	ProductPageMD  func() string
2079	Schema         func() string
2080	Cart           func() string
2081	XMLSitemap     func() string
2082	Wasm           func() string
2083	Clock          func() string
2084	AboutPage      func() string
2085	PolicyPage     func() string
2086	LinksPage      func() string
2087	CheckoutPage   func() string
2088	CompletePage   func() string
2089	CheckoutCSS    func() string
2090	StyleCSS       func() string
2091	COVIDPage      func() string
2092}
2093
2094var h = htmlTemplate{
2095	Empty:           func() string { return mustReadFileToString("htmpl/empty.html") },
2096	Head:           func() string { return mustReadFileToString("htmpl/head.html") },
2097	Logo:           func() string { return mustReadFileToString("htmpl/logo.html") },
2098	Header:         func() string { return mustReadFileToString("htmpl/header.html") },
2099	Categories:     func() string { return mustReadFileToString("htmpl/categories.html") },
2100	CatSubcats:     func() string { return mustReadFileToString("htmpl/catsubcats.html") },
2101	Footer:         func() string { return mustReadFileToString("htmpl/footer.html") },
2102	MainPage:       func() string { return mustReadFileToString("htmpl/main.html") },
2103	AuxPage:        func() string { return mustReadFileToString("htmpl/aux.html") },
2104	FrontPage:      func() string { return mustReadFileToString("htmpl/front.html") },
2105	CategoryPage:   func() string { return mustReadFileToString("htmpl/category.html") },
2106	CategoryPageMD: func() string { return mustReadFileToString("htmpl/category.md") },
2107	ProductPage:    func() string { return mustReadFileToString("htmpl/product.html") },
2108	ProductPageMD:  func() string { return mustReadFileToString("htmpl/product.md") },
2109	Schema:         func() string { return mustReadFileToString("htmpl/schema.html") },
2110	Cart:           func() string { return mustReadFileToString("htmpl/cart.html") },
2111	XMLSitemap:     func() string { return mustReadFileToString("htmpl/sitemap.xml") },
2112	Wasm:           func() string { return mustReadFileToString("htmpl/wasm.html") },
2113	CompletePage:   func() string { return mustReadFileToString("htmpl/complete.html") },
2114	Clock:          func() string { return mustReadFileToString("content/clock.html") },
2115	AboutPage:      func() string { return mustReadFileToString("content/about.html") },
2116	PolicyPage:     func() string { return mustReadFileToString("content/policy.html") },
2117	LinksPage:      func() string { return mustReadFileToString("content/links.html") },
2118	CheckoutPage:   func() string { return mustReadFileToString("content/checkout.html") },
2119	CheckoutCSS:    func() string { return mustReadFileToString("content/checkout.css") },
2120	StyleCSS:       func() string { return mustReadFileToString("content/style.css") },
2121	COVIDPage:      func() string { return mustReadFileToString("content/mementomori.html") },
2122}
2123
2124var htmlPageTemplateData htmlTemplateData
2125
2126var funcs = htmpl.FuncMap{
2127	"replace": replace, "mul": mul, "div": div, "safeHTML": safeHTML,
2128	"safeJS": safeJS, "stripProtocol": stripProtocol, "add": add, "sub": sub,
2129	"toFloat": toFloat, "equalsIgnoreCase": equalsIgnoreCase,
2130	"getsubcats": getsubcats, "escapesubcat": escapesubcat,
2131	"sortsubcats": sortsubcats, "repeat": repeat, "subcatlink": subcatlink,
2132}
2133
2134func mainTmpl() (tmpl *htmpl.Template, err error) {
2135	tmpl = htmpl.New("index").Funcs(funcs)
2136	if _, err := tmpl.Parse(h.MainPage()); err != nil {
2137		log.Println("Error parsing index template:", err)
2138		return tmpl, err
2139	}
2140
2141	partials := []struct {
2142		Name    string
2143		Content string
2144	}{
2145		{"head", h.Head()},
2146		{"schema", h.Schema()},
2147		{"header", h.Header()},
2148		{"catsubcats", h.CatSubcats()},
2149		{"categories", h.Categories()},
2150		{"footer", h.Footer()},
2151		{"cart", h.Cart()},
2152		{"wasm", h.Wasm()},
2153	}
2154
2155	for _, p := range partials {
2156		if _, err := tmpl.New(p.Name).Parse(p.Content); err != nil {
2157			log.Printf("Error parsing %s template: %v", p.Name, err)
2158			return tmpl, err
2159		}
2160	}
2161	return tmpl, err
2162}
2163
2164func auxTmpl() (tmpl *htmpl.Template, err error) {
2165	tmpl = htmpl.New("index").Funcs(funcs)
2166	if _, err := tmpl.Parse(h.AuxPage()); err != nil {
2167		log.Println("Error parsing index template:", err)
2168		return tmpl, err
2169	}
2170
2171	partials := []struct {
2172		Name    string
2173		Content string
2174	}{
2175		{"head", h.Head()},
2176		{"schema", h.Empty()},
2177		{"wasm", h.Empty()},
2178	}
2179
2180	for _, p := range partials {
2181		if _, err := tmpl.New(p.Name).Parse(p.Content); err != nil {
2182			log.Printf("Error parsing %s template: %v", p.Name, err)
2183			return tmpl, err
2184		}
2185	}
2186	return tmpl, err
2187}
2188
2189func pageMeta(c fiber.Ctx, base htmlTemplateData) htmlTemplateData {
2190	h := base
2191	host := string(c.Request().Host())
2192	/*
2193		proto := "http"
2194		if c.Secure() {
2195			proto += "s"
2196		}
2197	*/
2198	proto := "https"
2199	h.Canonical = proto + "://" + host + c.OriginalURL()
2200	h.BaseURL = proto + "://" + host
2201	h.RequestHost = host
2202	h.Protocol = proto
2203	h.CatsCounts, h.Cats, h.SubCatsCounts, h.SubCatsByCat = getcategories(allproducts)
2204	h.LenAllProducts = len(allproducts)
2205	h.Time = time.Now().Format(time.RFC3339Nano)
2206	h.Year = fmt.Sprintf("%v", time.Now().Year())
2207	h.MetaDesc = f.Sitemeta
2208	h.KeyWords = strings.Replace(f.Sitelongname, " ", ", ", -1)
2209	return h
2210}
2211
2212func initTMPL() {
2213	htmlPageTemplateData = htmlTemplateData{
2214		TestMode:           f.Teststripekey,
2215		Title:              f.Sitelongname,
2216		StripePK:           f.StripePK,
2217		SiteName:           f.Sitedomain,
2218		SiteTagLine:        f.Sitetagline,
2219		SiteName1:          htmpl.HTML(checkerBoard(f.Sitedomain)), //nolint
2220		SiteLongName:       f.Sitelongname,
2221		SiteASCIILogo:      htmpl.HTML(f.SiteASCIILogo), //nolint
2222		SitePrettyName:     f.Siteprettyname,
2223		SitePrettyNameCap:  f.Siteprettynamecap,
2224		SitePrettyNameCaps: f.Siteprettynamecaps,
2225		TelegramContact:    f.Tgcontact,
2226		TelegramChannel:    f.Tgchannel,
2227		WasmExecPath:       f.WasmExecPath,
2228		WasmExecRel:        f.WasmExecPath,
2229		Cats:               getcats(),
2230		LenAllProducts:     len(allproducts),
2231		ImgSRC: func() (ret string) {
2232			ret = f.Siteimagesrc
2233			if ret == "" {
2234				ret = "/i"
2235			}
2236			return ret
2237		}(),
2238		Page: "front",
2239		Time: time.Now().Format(time.RFC3339Nano),
2240		Year: fmt.Sprintf("%v", time.Now().Year()),
2241	}
2242	htmlPageTemplateData.CatsCounts, htmlPageTemplateData.Cats, htmlPageTemplateData.SubCatsCounts, htmlPageTemplateData.SubCatsByCat = getcategories(allproducts)
2243	htmlPageTemplateData.WasmBinary = wasmBinary()
2244
2245}
2246
2247func wasmBinary() (ret []string) {
2248	if len(f.WasmSRC) == 0 {
2249		return ret
2250	}
2251	if f.UseTinygo {
2252		for _, wasmSRC := range f.WasmSRC {
2253			outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + "-tiny.wasm"
2254			ret = append(ret, outputFile)
2255		}
2256		return ret
2257	}
2258	for _, wasmSRC := range f.WasmSRC {
2259		outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + ".wasm"
2260		ret = append(ret, outputFile)
2261	}
2262	return ret
2263}
2264
2265type xmlTemplateData struct {
2266	Cats         []string
2267	SubCatsByCat map[string][]string
2268	Products     p.Products
2269	Update       string
2270}
2271
2272func generateSitemapXML() string {
2273	xmlSitemapTemplateData := xmlTemplateData{
2274		Products: allproducts,
2275		Update:   time.Now().Format("2006-01-02"),
2276	}
2277	_, xmlSitemapTemplateData.Cats, _, xmlSitemapTemplateData.SubCatsByCat = getcategories(allproducts)
2278	var err1 error
2279	xtmpl, err1 := ttmpl.New("index").Funcs(ttmpl.FuncMap{"getsubcats": getsubcats}).Parse(h.XMLSitemap())
2280	if err1 != nil {
2281		log.Println("Error parsing index template:", err1)
2282	}
2283	var result bytes.Buffer
2284	err1 = xtmpl.Execute(&result, xmlSitemapTemplateData)
2285	if err1 != nil {
2286		log.Println("error: ", err1)
2287	}
2288	return result.String()
2289}
2290
2291func toFloat(s string) float64 {
2292	if s == "" {
2293		return 0.0
2294	}
2295	f, err := strconv.ParseFloat(s, 64)
2296	if err != nil {
2297		return 0.0
2298	}
2299	return f
2300}
2301
2302func checkerBoard(input string) string {
2303	var result strings.Builder
2304	for i, char := range input {
2305		// Wrap every other letter with the specified HTML
2306		if i%2 == 0 {
2307			result.WriteString(fmt.Sprintf("<span class='nv'>%c</span>", char))
2308		} else {
2309			result.WriteRune(char)
2310		}
2311	}
2312	return result.String()
2313}
2314
2315type htmlTemplateData struct {
2316	Title              string
2317	MetaDesc           string
2318	Canonical          string
2319	BaseURL            string
2320	ImgSRC             string // url where images are hosted
2321	OrdersURL          string // url where checkout is served from
2322	SiteName           string
2323	SiteTagLine        string
2324	SiteName1          htmpl.HTML //checkerboard - alternate swap text & bg color
2325	SiteLongName       string
2326	SitePrettyName     string //π•„π•’π•˜π•Ÿπ•–π•₯𝕠𝕀𝕑𝕙𝕖𝕣𝕖.π•Ÿπ•–π•₯
2327	SitePrettyNameCap  string //π•„π•’π•˜π•Ÿπ•–π•₯𝕠𝕀𝕑𝕙𝕖𝕣𝕖.π•Ÿπ•–π•₯
2328	SitePrettyNameCaps string //π•„π”Έπ”Ύβ„•π”Όπ•‹π•†π•Šβ„™β„π”Όβ„π”Ό.ℕ𝔼𝕋
2329	SiteASCIILogo      htmpl.HTML
2330	TelegramContact    string
2331	TelegramChannel    string
2332	Protocol           string
2333	RequestHost        string
2334	KeyWords           string
2335	Style              htmpl.HTML
2336	Heading            htmpl.HTML
2337	StripePK           string
2338	Cats               []string
2339	CatsCounts         map[string]int
2340	SubCatsCounts      map[string]map[string]int
2341	SubCatsByCat       map[string][]string
2342	LenAllProducts     int
2343	Mobile             bool
2344	Gocanvas           htmpl.HTML
2345	WasmBinary         []string
2346	WasmExecPath       string
2347	WasmExecRel        string
2348	StyleFontFace      htmpl.CSS
2349	Message            htmpl.HTML
2350	Page               string
2351	Year               string
2352	Time               string
2353	AboutHTML          htmpl.HTML
2354	LinksHTML          htmpl.HTML
2355	PolicyHTML         htmpl.HTML
2356	TestMode           bool
2357}
2358
2359func equalsIgnoreCase(a, b string) bool {
2360	return strings.EqualFold(strings.Join(strings.Fields(a), ""), strings.Join(strings.Fields(b), ""))
2361}
2362
2363func replace(s, o, n string) string {
2364	return strings.ReplaceAll(s, o, n)
2365}
2366func mul(a, b float64) float64 {
2367	return a * b
2368}
2369func div(a, b float64) float64 {
2370	return a / b
2371}
2372func add(a, b int) int {
2373	return a + b
2374}
2375func sub(a, b int) int {
2376	return a - b
2377}
2378func safeHTML(s string) htmpl.HTML {
2379	return htmpl.HTML(s) //nolint
2380}
2381func safeJS(s string) htmpl.JS {
2382	return htmpl.JS(s) //nolint
2383}
2384func stripProtocol(s string) string {
2385	return strings.Replace(strings.Replace(s, "https://", "", -1), "http://", "", -1)
2386}
2387func repeat(s string, count int) string {
2388	var result string
2389	for i := 0; i < count; i++ {
2390		result += s
2391	}
2392	return result
2393}
2394func sortsubcats(subcats []string, counts map[string]map[string]int) []string {
2395	sort.Slice(subcats, func(i, j int) bool {
2396		catI, catJ := subcats[i], subcats[j]
2397		countI, countJ := counts[catI]["count"], counts[catJ]["count"]
2398		return countI > countJ
2399	})
2400	return subcats
2401}
2402
2403func subcatlink(subcategory string) string {
2404	s := subcategory
2405	s = strings.ReplaceAll(s, "ΒΌ", "quarter-")
2406	s = strings.ReplaceAll(s, "Β½", "half-")
2407	s = strings.ReplaceAll(s, "1/16", "sixteenth-")
2408	s = strings.ReplaceAll(s, "%", "-pct")
2409	s = strings.ReplaceAll(s, "  ", " ")
2410	s = strings.ReplaceAll(s, "watt1", "watt-1")
2411	s = strings.ReplaceAll(s, "watt5", "watt-5")
2412	s = strings.ReplaceAll(s, " ", "-")
2413	s = strings.ReplaceAll(s, "--", "-")
2414	return s
2415}
2416
2417
2418// ===== wasm.go =====
2419// Package main wasm.go
2420package main
2421
2422import (
2423	"fmt"
2424	"log"
2425	"path/filepath"
2426	"strings"
2427	"time"
2428
2429	"github.com/bitfield/script"
2430	"github.com/briandowns/spinner"
2431)
2432
2433func compileWASM() {
2434	s := spinner.New(spinner.CharSets[14], 25*time.Millisecond)
2435	s.Suffix = " Compiling wasm..."
2436	for _, wasmSRC := range f.WasmSRC {
2437		ascend := strings.Repeat("../", len(strings.Split(wasmSRC, "/")))
2438		outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + ".wasm"
2439		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)
2440		log.Println("Compiling wasm with:")
2441		log.Println(compilecmd)
2442		s.Start()
2443		_, err := script.Exec(compilecmd).Stdout()
2444		if err != nil {
2445			log.Fatal(err)
2446		}
2447		s.Stop()
2448		log.Println("Compiled wasm!")
2449	}
2450	for _, wasmSRC := range f.WasmSRC {
2451		ascend := strings.Repeat("../", len(strings.Split(wasmSRC, "/")))
2452		outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + "-tiny.wasm"
2453		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)
2454		log.Println("compiling wasm with:")
2455		log.Println(compilecmd)
2456		s.Start()
2457		_, err := script.Exec(compilecmd).Stdout()
2458		if err != nil {
2459			log.Fatal(err)
2460		}
2461		s.Stop()
2462		log.Println("Compiled wasm!")
2463	}
2464}
2465
2466func ldflags(s string) (ss string) {
2467	checkFiles, err := script.FindFiles(s).Slice()
2468	if err != nil {
2469		log.Fatal(err)
2470	}
2471	if f.LDFlagsX != "" {
2472		for _, s := range checkFiles {
2473			res, err := script.File(s).Match(strings.Split(f.LDFlagsX, "=")[0]).String()
2474			if err != nil {
2475				log.Fatal(err)
2476			}
2477			if res != "" {
2478				ss += fmt.Sprintf(` -X 'main.%s' `, f.LDFlagsX)
2479				break
2480			}
2481		}
2482	}
2483	for _, s := range checkFiles {
2484		res, err := script.File(s).Match("wasmName").String()
2485		if err != nil {
2486			log.Fatal(err)
2487		}
2488		if res != "" {
2489			ss += fmt.Sprintf(` -X 'main.wasmName=%s' `, strings.TrimSuffix(filepath.Base(s), filepath.Ext(s))+".wasm")
2490			break
2491		}
2492	}
2493	if ss != "" {
2494		ss = `-ldflags="` + ss + `"`
2495	}
2496	return ss
2497}
2498
2499