github.com/lolorenzo777/zazzy@v0.4.3/zazzy.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"log"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"sort"
    15  	"strings"
    16  	"text/template"
    17  	"time"
    18  
    19  	"github.com/gobwas/glob"
    20  	"github.com/lolorenzo777/loadfavicon/getfavicon"
    21  	"github.com/russross/blackfriday/v2"
    22  	"gopkg.in/yaml.v3"
    23  )
    24  
    25  const (
    26  	ZSDIR  = ".zazzy"
    27  	DFTPUBDIR = ".pub"
    28  )
    29  
    30  var PUBDIR string = DFTPUBDIR
    31  
    32  type Vars map[string]string
    33  
    34  // renameExt renames extension (if any) from oldext to newext
    35  // If oldext is an empty string - extension is extracted automatically.
    36  // If path has no extension - new extension is appended
    37  func renameExt(path, oldext, newext string) string {
    38  	if oldext == "" {
    39  		oldext = filepath.Ext(path)
    40  	}
    41  	if oldext == "" || strings.HasSuffix(path, oldext) {
    42  		return strings.TrimSuffix(path, oldext) + newext
    43  	} else {
    44  		return path
    45  	}
    46  }
    47  
    48  // globals returns list of global OS environment variables that start
    49  // with ZS_ prefix as Vars, so the values can be used inside templates
    50  func globals() Vars {
    51  	vars := Vars{}
    52  	for _, e := range os.Environ() {
    53  		pair := strings.Split(e, "=")
    54  		if strings.HasPrefix(pair[0], "ZS_") {
    55  			vars[strings.ToLower(pair[0][3:])] = pair[1]
    56  		}
    57  	}
    58  
    59  	// special environment variable
    60  	if len(vars["pubdir"]) != 0 {
    61  		PUBDIR = vars["pubdir"]
    62  	}
    63  	if len(vars["favicondir"]) == 0 {
    64  		vars["favicondir"] = "/img/favicons"
    65  	}
    66  
    67  	return vars
    68  }
    69  
    70  // load .zazzy/.ignore file with list of files and directories to be ignored during the process
    71  // each entry must be formatted as a glob pattern https://github.com/gobwas/glob
    72  // return an array of trimed pattern of files to ignore
    73  func loadIgnore() (lst []string) {
    74      f, err := os.Open(filepath.Join(ZSDIR, ".ignore"))
    75      if err != nil {
    76  		// .ignore file is not mandatory
    77          return nil
    78      }
    79      defer f.Close()
    80  
    81      // read the file line by line using scanner
    82      scanner := bufio.NewScanner(f)
    83  
    84      for scanner.Scan() {
    85  		entry := strings.Trim(scanner.Text(), " ")
    86  		if len(entry)>0 && entry[:1] != "#" {
    87  			if _, err := glob.Compile(entry); err != nil {
    88  				log.Println(err)
    89  			} else {
    90  				lst = append(lst, entry)
    91  			}
    92  		}
    93      }
    94  
    95  	// ensure PUBDIR is always ignored
    96  	if filepath.Base(PUBDIR)[0] != '.' && !strings.HasPrefix(PUBDIR, ".") {
    97  		pubdir := strings.TrimRight(PUBDIR, "/") + "**"
    98  		lst = append(lst, pubdir)
    99  	}
   100  	return lst
   101  }
   102  
   103  var gSitemapWarning bool
   104  
   105  // appendSitemap generate an entry in the sitemap.txt file
   106  // according to paramaters: ZS_SITEMAPTXT must be true, and 
   107  // the "sitemap: true" is in the YAML file header
   108  func appendSitemap(path string, vars Vars) {
   109  	if strings.ToLower(vars["sitemaptype"]) != "txt" {
   110  		return 
   111  	}
   112  
   113  	if strings.ToLower(vars["sitemap"]) != "true" {
   114  		return 
   115  	}
   116  
   117  	if len(vars["hosturl"]) == 0 && !gSitemapWarning {
   118  		gSitemapWarning = true
   119  		fmt.Println("Warning: generating sitemap without hosturl.")
   120  	}
   121  
   122  	sitemapentry := filepath.Join(vars["hosturl"], vars["url"])
   123  
   124      file, err := os.OpenFile(filepath.Join(PUBDIR, "sitemap.txt"), os.O_RDWR|os.O_CREATE, 0755)
   125      if err != nil {
   126  		log.Println(err)
   127  		return
   128      }
   129      defer file.Close()
   130  	scanner := bufio.NewScanner(file)
   131      for scanner.Scan() {
   132  		// do not add twice the same URL
   133  		if strings.ToLower(strings.Trim(scanner.Text(), " ")) == sitemapentry {
   134  			return 
   135  		}
   136      }
   137      if err := scanner.Err(); err != nil {
   138  		log.Println(err)
   139  		return
   140      }
   141  	if _, err := file.WriteString( sitemapentry +"\n"); err != nil {
   142  		log.Println(err)
   143  		return
   144  	}
   145  }
   146  
   147  // run executes a command or a script. Vars define the command environment,
   148  // each zs var is converted into OS environemnt variable with ZS_ prefix
   149  // prepended.  Additional variable $ZS contains path to the zs binary. Command
   150  // stderr is printed to zs stderr, command output is returned as a string.
   151  func run(vars Vars, cmd string, args ...string) (string, error) {
   152  	// external commande (plugin)
   153  	var errbuf, outbuf bytes.Buffer
   154  	c := exec.Command(cmd, args...)
   155  	env := []string{"ZS=" + os.Args[0], "ZS_OUTDIR=" + PUBDIR}
   156  	env = append(env, os.Environ()...)
   157  	for k, v := range vars {
   158  		env = append(env, "ZS_"+strings.ToUpper(k)+"="+v)
   159  	}
   160  	c.Env = env
   161  	c.Stdout = &outbuf
   162  	c.Stderr = &errbuf
   163  
   164  	err := c.Run()
   165  
   166  	if errbuf.Len() > 0 {
   167  		log.Println("ERROR:", errbuf.String())
   168  	}
   169  	if err != nil {
   170  		return "", err
   171  	}
   172  	return outbuf.String(), nil
   173  }
   174  
   175  // getDownloadedFavicon get favicon URL of the downloaded Favicon, and download it 
   176  // if it doesn't exist on the local directory.
   177  func getDownloadedFavicon(website string) (url string, err error) {
   178  
   179  	vars := globals()
   180  	faviconCachePath := filepath.Join(PUBDIR, vars["favicondir"])
   181  	faviconSlugifiedWebsite := getfavicon.SlugHost(website)
   182  
   183  	// look if favicon(s) has already been downloaded
   184  	cache, err := filepath.Glob(filepath.Join(faviconCachePath, faviconSlugifiedWebsite) + "+*.*")
   185  	if len(cache) == 0 && err == nil{
   186  		// Connect to the website and download the best favicon
   187  		favicons, err := getfavicon.Download(website, faviconCachePath, true)
   188  		if len(favicons) == 0 {
   189  			log.Println(err)
   190  			return "", err
   191  		}
   192  		url = filepath.Join("/", vars["favicondir"], favicons[0].DiskFileName)
   193  	} else {
   194  		url = cache[0]
   195  		if url[:len(PUBDIR)] != PUBDIR {
   196  			panic("getDownloadedFavicon")
   197  		}
   198  		url = url[len(PUBDIR):]
   199  	}
   200  
   201  	return url, err
   202  }
   203  
   204  // renderFavicon donwload th favicon of a website given in paramaters 
   205  // and generate html to render thefavicon image. 
   206  func renderFavicon(vars Vars, args ...string) (string, error){
   207  	if len(args) != 1 {
   208  		log.Println("favicon placeholder requires a website in parameter. nothing rendered")
   209  		return "", nil
   210  	}
   211  
   212  	faviconURL, err := getDownloadedFavicon(args[0])
   213  	if len(faviconURL) > 0 && err == nil {
   214  		return "<img src=\"" + faviconURL +"\" alt=\"icon\" class=\"favicon\" role=\"img\">", nil
   215  	}
   216  	return "", err
   217  }
   218  
   219  // renderlist generate an HTML string for every files in the pattern 
   220  // passed in arg[0]. The string if rendered according to the itemlayout.html file.
   221  // Than all strings are concatenated and ordered accordng to filenames in the pattern
   222  func renderlist(vars Vars, args ...string) (string, error){
   223  	// get the pattern of files to scan and list
   224  	if len(args) != 1 {
   225  		log.Println("renderlist placeholder requires pattern in parameter. nothing rendered.")
   226  		return "", nil
   227  	}
   228  	filelistpattern := args[0]
   229  
   230  	// check the pattern and get lisy of corresponding files
   231  	matchingfiles, err := filepath.Glob(filelistpattern)
   232  	if err != nil {
   233  		return "", errors.New("bad pattern")
   234  	}
   235  	if len(matchingfiles) == 0 {
   236  		fmt.Println("renderlist: no files corresponds to this pattern. The list is empty.", err)
   237  		return "", errors.New("bad pattern")
   238  	}
   239  	sort.Sort(sort.Reverse(sort.StringSlice(matchingfiles)))
   240  	// get list of files to ignore
   241  	ignorelist := loadIgnore()
   242  
   243  	// get the layout for items
   244  	if _, ok := vars["itemlayout"]; !ok {
   245  		vars["itemlayout"] = filepath.Join(ZSDIR, "itemlayout.html")
   246  	}
   247  	_, itemlayout, err := getVars(vars["itemlayout"], vars)
   248  	if err != nil {
   249  		fmt.Println("unable to proceed item layout file:", err)
   250  	}
   251  
   252  	// scan all existing files, and process as a list item
   253  	result := ""
   254  	for _, path := range matchingfiles {
   255  		// ignore hidden files and directories
   256  		if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") {
   257  			continue
   258  		}
   259  		
   260  		// ignore files and directory listed in the .zazzy/.ignore file
   261  		for _, ignoreentry := range ignorelist {
   262  			g, _ := glob.Compile(ignoreentry)
   263  			if g.Match(path) {
   264  				fmt.Printf("renderlist item ignored: %q", path)
   265  				continue
   266  			}
   267  		}
   268  
   269  		// inform user about fs errors, but continue iteration
   270  		info, err := os.Stat(path)
   271  		if err != nil {
   272  			fmt.Println("renderlist item error:", err)
   273  			continue
   274  		}
   275  
   276  		if info.IsDir() {
   277  			continue
   278  		} else {
   279  			log.Println("renderlist item:", path)
   280  			// load file's vars
   281  			vitem, _, err := getVars(path, vars)
   282  			if err != nil {
   283  				fmt.Println("renderlist item error:", err)
   284  				return "", err
   285  			}
   286  			vitem["file"] = path
   287  			vitem["url"] = path[:len(path)-len(filepath.Ext(path))] + ".html"
   288  			vitem["output"] = filepath.Join(PUBDIR, vitem["url"])
   289  			item, err := render(itemlayout, vitem, 1)
   290  			if err != nil {
   291  				return "", err
   292  			}
   293  			result += item
   294  		}
   295  	}
   296  	return result, nil
   297  }
   298  
   299  
   300  // getVars returns list of variables defined in a text file and actual file
   301  // content following the variables declaration. Header is separated from
   302  // content by an empty line. Header can be either YAML or JSON.
   303  // If no empty newline is found - file is treated as content-only.
   304  func getVars(path string, globals Vars) (Vars, string, error) {
   305  	b, err := ioutil.ReadFile(path)
   306  	if err != nil {
   307  		return nil, "", err
   308  	}
   309  	s := string(b)
   310  
   311  	// Pick some default values for content-dependent variables
   312  	v := Vars{}
   313  	title := strings.Replace(strings.Replace(path, "_", " ", -1), "-", " ", -1)
   314  	v["title"] = strings.ToTitle(title)
   315  	v["description"] = ""
   316  	v["file"] = path
   317  	v["url"] = path[:len(path)-len(filepath.Ext(path))] + ".html"
   318  	v["output"] = filepath.Join(PUBDIR, v["url"])
   319  
   320  	// Override default values with globals
   321  	for name, value := range globals {
   322  		v[name] = value
   323  	}
   324  
   325  	// Add layout if none is specified
   326  	if _, ok := v["layout"]; !ok {
   327  		v["layout"] = "layout.html"
   328  	}
   329  
   330  	delim := "\n---\n"
   331  	if sep := strings.Index(s, delim); sep == -1 {
   332  		return v, s, nil
   333  	} else {
   334  		header := s[:sep]
   335  		body := s[sep+len(delim):]
   336  
   337  		vars := Vars{}
   338  		if err := yaml.Unmarshal([]byte(header), &vars); err != nil {
   339  			fmt.Println("ERROR: failed to parse header", err)
   340  			return nil, "", err
   341  		} else {
   342  			// Override default values + globals with the ones defines in the file
   343  			for key, value := range vars {
   344  				v[key] = value
   345  			}
   346  		}
   347  		v["url"] = strings.TrimLeft(v["url"], "./")
   348  		//if strings.HasPrefix(v["url"], "./") {
   349  		//	v["url"] = v["url"][2:]
   350  		//}
   351  		return v, body, nil
   352  	}
   353  }
   354  
   355  // Render expanding zs plugins and variables, and process special command
   356  func render(s string, vars Vars, deep int) (string, error) {
   357  	delim_open := "{{"
   358  	delim_close := "}}"
   359  
   360  	out := &bytes.Buffer{}
   361  	for {
   362  		if from := strings.Index(s, delim_open); from == -1 {
   363  			out.WriteString(s)
   364  			return out.String(), nil
   365  		} else {
   366  			if to := strings.Index(s, delim_close); to == -1 {
   367  				return "", fmt.Errorf("close delim not found")
   368  			} else {
   369  				out.WriteString(s[:from])
   370  				cmd := s[from+len(delim_open) : to]
   371  				s = s[to+len(delim_close):]
   372  				m := strings.Fields(cmd)
   373  				// proceed with special commands
   374  				switch {
   375  				case m[0] == "renderlist": 
   376  					if res, err := renderlist(vars, m[1:]...); err == nil {
   377  						out.WriteString(res)
   378  					} else {
   379  						fmt.Println(err)
   380  					}
   381  					continue
   382  				case m[0] == "favicon" :
   383  					if res, err := renderFavicon(vars, m[1:]...); err == nil {
   384  						out.WriteString(res)
   385  					} else {
   386  						fmt.Println(err)
   387  					}
   388  					continue
   389  				case filepath.Ext(m[0]) == ".html" || filepath.Ext(m[0]) == ".md":
   390  					// proceed partials (.html or md) 
   391  					if b, err := ioutil.ReadFile(filepath.Join(ZSDIR, m[0])); err == nil {
   392  						// make it recursive
   393  						if deep > 10 {
   394  							return string(b), nil
   395  						}
   396  						if res, err := render(string(b), vars, deep+1); err == nil {
   397  							out.WriteString(res)
   398  						} else {
   399  							fmt.Println(err)
   400  						}
   401  						continue
   402  					}
   403  					fallthrough
   404  				case len(m) == 1 :
   405  					// variable
   406  					if v, ok := vars[m[0]]; ok {
   407  						out.WriteString(v)
   408  						continue
   409  					}
   410  				}
   411  
   412  				// sz pluggins 
   413  				if res, err := run(vars, m[0], m[1:]...); err == nil {
   414  					out.WriteString(res)
   415  				} else {
   416  					fmt.Println(err)
   417  				}
   418  			}
   419  		}
   420  	}
   421  }
   422  
   423  // Renders markdown with the given layout into html expanding all the macros
   424  func buildMarkdown(path string, w io.Writer, vars Vars) error {
   425  	v, body, err := getVars(path, vars)
   426  	if err != nil {
   427  		return err
   428  	}
   429  	content, err := render(body, v, 1)
   430  	if err != nil {
   431  		return err
   432  	}
   433  	v["content"] = string(blackfriday.Run([]byte(content)))
   434  	if w == nil {
   435  		out, err := os.Create(filepath.Join(PUBDIR, renameExt(path, "", ".html")))
   436  		if err != nil {
   437  			return err
   438  		}
   439  		defer out.Close()
   440  		w = out
   441  	}
   442  	appendSitemap(path, v)
   443  
   444  	// process layout only if it exists
   445  	layoutfile := filepath.Join(ZSDIR, v["layout"])
   446  	_, errlayout := os.Stat(layoutfile)
   447  	if errors.Is(errlayout, os.ErrNotExist) {
   448  		_, err = io.WriteString(w, v["content"])
   449  		return err
   450  	} else if errlayout != nil {
   451  		return errlayout
   452  	}
   453  
   454  	return buildHTML(filepath.Join(ZSDIR, v["layout"]), w, v)
   455  }
   456  
   457  // Renders text file expanding all variable macros inside it
   458  func buildHTML(path string, w io.Writer, vars Vars) error {
   459  	v, body, err := getVars(path, vars)
   460  	if err != nil {
   461  		return err
   462  	}
   463  	if body, err = render(body, v, 1); err != nil {
   464  		return err
   465  	}
   466  	tmpl, err := template.New("").Delims("<%", "%>").Parse(body)
   467  	if err != nil {
   468  		return err
   469  	}
   470  	if w == nil {
   471  		f, err := os.Create(filepath.Join(PUBDIR, path))
   472  		if err != nil {
   473  			return err
   474  		}
   475  		defer f.Close()
   476  		w = f
   477  	}
   478  	appendSitemap(path, v)
   479  
   480  	return tmpl.Execute(w, vars)
   481  }
   482  
   483  // Copies file as is from path to writer
   484  func buildRaw(path string, w io.Writer) error {
   485  	in, err := os.Open(path)
   486  	if err != nil {
   487  		return err
   488  	}
   489  	defer in.Close()
   490  	if w == nil {
   491  		if out, err := os.Create(filepath.Join(PUBDIR, path)); err != nil {
   492  			return err
   493  		} else {
   494  			defer out.Close()
   495  			w = out
   496  		}
   497  	}
   498  	_, err = io.Copy(w, in)
   499  	return err
   500  }
   501  
   502  func build(path string, w io.Writer, vars Vars) error {
   503  	ext := filepath.Ext(path)
   504  	var err error
   505  	if ext == ".md" || ext == ".mkd" {
   506  		err = buildMarkdown(path, w, vars)
   507  	} else if ext == ".html" || ext == ".xml" {
   508  		err = buildHTML(path, w, vars)
   509  	} else {
   510  		err = buildRaw(path, w)
   511  	}
   512  	if err != nil {
   513  		log.Println(err)
   514  	}
   515  	return err
   516  }
   517  
   518  func buildAll(watch bool) {
   519  	lastModified := time.Unix(0, 0)
   520  	modified := false
   521  
   522  	vars := globals()
   523  	ignorelist := loadIgnore()
   524  	// clear sitemap if any
   525  	os.Remove(filepath.Join(PUBDIR, "sitemap.txt"))
   526  
   527  	for {
   528  		os.Mkdir(PUBDIR, 0755)
   529  		filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
   530  			// ignore hidden files and directories
   531  			if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") {
   532  				return nil
   533  			}
   534  			
   535  			// ignore files and directory listed in the .zazzy/.ignore file
   536  			for _, ignoreentry := range ignorelist {
   537  				g, _ := glob.Compile(ignoreentry)
   538  				if g.Match(path) {
   539  					return nil
   540  				}
   541  			}
   542  
   543  			// inform user about fs walk errors, but continue iteration
   544  			if err != nil {
   545  				fmt.Println("buildAll error:", err)
   546  				return nil
   547  			}
   548  
   549  			if info.IsDir() {
   550  				os.Mkdir(filepath.Join(PUBDIR, path), 0755)
   551  				return nil
   552  			} else if info.ModTime().After(lastModified) {
   553  				if !modified {
   554  					// First file in this build cycle is about to be modified
   555  					run(vars, "buildAll prehook")
   556  					modified = true
   557  				}
   558  				log.Println("build:", path)
   559  				return build(path, nil, vars)
   560  			}
   561  			return nil
   562  		})
   563  		if modified {
   564  			// At least one file in this build cycle has been modified
   565  			run(vars, "buildAll posthook")
   566  			modified = false
   567  		}
   568  		if !watch {
   569  			break
   570  		}
   571  		lastModified = time.Now()
   572  		time.Sleep(1 * time.Second)
   573  	}
   574  }
   575  
   576  func generateFile(path string, templatetext string, data any) (err error) {
   577  	tmp, _ := template.New("template").Parse(templatetext)
   578  	flayout, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
   579  	if err != nil {
   580  		fmt.Println(err)
   581  		return err
   582  	}
   583  	defer flayout.Close()
   584  	if err = tmp.Execute(flayout, data); err != nil {
   585  		fmt.Println(err)
   586  	}
   587  	return err
   588  }
   589  
   590  // generateNewWebsite create basic files in the current directory for a new website
   591  // with a basic layout
   592  // 
   593  // Parameters
   594  //
   595  // githubpages: 
   596  // vscode: create build & watch tasks 
   597  // 
   598  func generateNewWebsite(title string, hosturl string, vscode bool, githubpages bool, sitemap bool) {
   599  	hosturl = strings.ToLower(strings.Trim(hosturl, " "))
   600  	// .zazzy  
   601  	err := os.Mkdir(".zazzy", 0755)
   602  	if err != nil && os.IsExist(err) {
   603  		log.Println(".zazzy directory already exists. init process stops.")
   604  		return
   605  	}
   606  	os.Mkdir("css", 0755)
   607  	os.Mkdir("img", 0755)
   608  	os.Mkdir("js", 0755)
   609  
   610  	type TWebsite struct {
   611  		Title string
   612  		Url string
   613  		Description string
   614  		Export string
   615  		Sitemap string
   616  	}
   617  	website := TWebsite{
   618  		Title: title, 
   619  		Url: hosturl,
   620  		Sitemap: "false",
   621  	}
   622  	// fulfill the export variable
   623  	if githubpages {
   624  		website.Export = "rm -r docs; ZS_PUBDIR=docs "
   625  	}
   626  	if sitemap {
   627  		website.Export += " ZS_SITEMAPTYPE=txt"
   628  		website.Sitemap = "true"
   629  	}
   630  	website.Export += " ZS_HOSTURL=" + hosturl
   631  	website.Export += " "
   632  
   633  	// tasks.json
   634  	tasksTemplate := `{
   635  		"version": "2.0.0",
   636  		"tasks": [
   637  			{
   638  				"label": "build",
   639  				"type": "shell",
   640  				"command": "{{ .Export }}zazzy build"
   641  			},
   642  			{
   643  				"label": "watch",
   644  				"type": "shell",
   645  				"command": "{{ .Export }}zazzy watch"
   646  			}
   647  		]
   648  	}`
   649  	if vscode {
   650  		os.Mkdir(".vscode", 0755)
   651  		generateFile(".vscode/tasks.json", tasksTemplate, website)
   652  	}
   653  
   654  	// index.md
   655  	indexTemplate := `title: {{ .Title }}
   656  url: {{ .Url }}
   657  sitemap: {{ .Sitemap }}
   658  ---
   659  
   660  # Home {{ .Title }}
   661  `
   662  	generateFile("index.md", indexTemplate, website)
   663  
   664  	// layout.html
   665  	layoutTemplate := `<!DOCTYPE html>
   666  <html lang="en">
   667  <head>
   668      <title>{{ .Title }}</title>
   669      <meta name="title" content="{{ .Title }}">
   670      <link rel="canonical" href="{{ .Url }}">
   671      <meta charset="utf-8">
   672      <meta name="viewport" content="width=device-width, initial-scale=1.0">
   673  
   674      <!--page-description-->
   675      <meta name="description" content="{{ .Description }}">
   676  
   677      <!--favicon-->
   678      <link rel="shortcut icon" href="/favicon.ico">
   679  </head>
   680  <body>
   681  {{"{{content}}"}}
   682  </body>
   683  </html>
   684  `
   685  	generateFile(".zazzy/layout.html", layoutTemplate, website)
   686  
   687  	if githubpages {
   688  		// .ignore
   689  		ignoreTemplate := `# files to ignore
   690  readme.md
   691  `
   692  		generateFile(".zazzy/.ignore", ignoreTemplate, website)				
   693  	}
   694  	fmt.Println("zazzy website generated")
   695  }
   696  
   697  func init() {
   698  	// prepend .zazzy to $PATH, so plugins will be found before OS commands
   699  	p := os.Getenv("PATH")
   700  	p = ZSDIR + ":" + p
   701  	os.Setenv("PATH", p)
   702  }
   703  
   704  func main() {
   705  	if len(os.Args) == 1 {
   706  		fmt.Println(os.Args[0], "<command> [args]")
   707  		return
   708  	}
   709  	cmd := os.Args[1]
   710  	args := os.Args[2:]
   711  	switch cmd {
   712  	case "build":
   713  		if len(args) == 0 {
   714  			buildAll(false)
   715  		} else if len(args) == 1 {
   716  			if err := build(args[0], os.Stdout, globals()); err != nil {
   717  				fmt.Println("ERROR: " + err.Error())
   718  			}
   719  		} else {
   720  			fmt.Println("ERROR: too many arguments")
   721  		}
   722  	case "watch":
   723  		buildAll(true)
   724  	case "init": {
   725  		if len(args) <= 2 {
   726  			fmt.Println("init: website title and host url expected")
   727  		} else {
   728  			fvscode := false
   729  			fgithubpages := false
   730  			fsitemap := false
   731  			for _, v := range(args[2:]) {
   732  				switch strings.ToLower(v) {
   733  				case "--vscode": fvscode = true
   734  				case "--githubpages": fgithubpages = true 
   735  				case "--sitemap": fsitemap = true
   736  				} 
   737  			}
   738  			generateNewWebsite(args[0], args[1], fvscode, fgithubpages, fsitemap) 
   739  		}
   740  	}
   741  	case "var":
   742  		if len(args) == 0 {
   743  			fmt.Println("var: filename expected")
   744  		} else {
   745  			s := ""
   746  			if vars, _, err := getVars(args[0], Vars{}); err != nil {
   747  				fmt.Println("var: " + err.Error())
   748  			} else {
   749  				if len(args) > 1 {
   750  					for _, a := range args[1:] {
   751  						s = s + vars[a] + "\n"
   752  					}
   753  				} else {
   754  					for k, v := range vars {
   755  						s = s + k + ":" + v + "\n"
   756  					}
   757  				}
   758  			}
   759  			fmt.Println(strings.TrimSpace(s))
   760  		}
   761  	default:
   762  		if s, err := run(globals(), cmd, args...); err != nil {
   763  			fmt.Println(err)
   764  		} else {
   765  			fmt.Println(s)
   766  		}
   767  	}
   768  }