github.com/vulppine/fotoDen@v0.3.0/tool/theme.go (about)

     1  package tool
     2  
     3  import (
     4  	"archive/zip"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/url"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"text/template"
    15  
    16  	"github.com/vulppine/fotoDen/generator"
    17  )
    18  
    19  // Note:
    20  // This new method of zipped themes should immediately
    21  // replace the old method of unzipping an uninitialized
    22  // theme into a folder. It should make things more
    23  // easier, as rather than having to initialize a theme
    24  // into a directory, all a user has to do in order
    25  // to create a site is just throw a --theme flag into
    26  // the command. Equally, a default theme should be
    27  // embed during build in case the user doesn't have
    28  // a custom theme.zip to use.
    29  //
    30  // The old method should be deprecated immediately.
    31  
    32  // theme represents a struct containing an open
    33  // zip archive for reading, plus a struct containing
    34  // an internal theme.json file. It also caches some
    35  // files for later use via the text/template system.
    36  type theme struct {
    37  	a *zip.Reader
    38  	s struct {
    39  		ThemeName   string
    40  		Stylesheets []string
    41  		Scripts     []string
    42  		Other       []string
    43  	}
    44  	f map[string]string // file cache
    45  }
    46  
    47  func zipFileReader(z string) (io.ReaderAt, int64, error) {
    48  	f, err := os.Open(z)
    49  	if checkError(err) {
    50  		return nil, 0, err
    51  	}
    52  
    53  	fi, err := f.Stat()
    54  	if checkError(err) {
    55  		return nil, 0, err
    56  	}
    57  
    58  	return f, fi.Size(), nil
    59  }
    60  
    61  func openTheme(z io.ReaderAt, zs int64) (*theme, error) {
    62  	var err error
    63  
    64  	t := new(theme)
    65  
    66  	t.a, err = zip.NewReader(z, zs)
    67  	if checkError(err) {
    68  		return nil, err
    69  	}
    70  
    71  	c, err := t.a.Open("theme.json")
    72  	if checkError(err) {
    73  		return nil, err
    74  	}
    75  
    76  	cb, err := io.ReadAll(c)
    77  	if checkError(err) {
    78  		return nil, err
    79  	}
    80  
    81  	json.Unmarshal(cb, &t.s)
    82  
    83  	verbose("theme config successfully opened")
    84  
    85  	t.f = make(map[string]string)
    86  	f := []string{
    87  		"album-template.html",
    88  		"folder-template.html",
    89  		"photo-template.html",
    90  		"page-template.html",
    91  	}
    92  
    93  	for _, i := range f {
    94  		f, err := t.a.Open(filepath.Join("html", i))
    95  		if err != nil {
    96  			verbose("could not open a required page template: " + i)
    97  			return nil, err
    98  		}
    99  
   100  		b, err := io.ReadAll(f)
   101  		if err != nil {
   102  			verbose("could not open a required page template: " + i)
   103  			return nil, err
   104  		}
   105  
   106  		t.f[i] = string(b)
   107  	}
   108  
   109  	return t, nil
   110  }
   111  
   112  // writeFile directly writes a file from a theme's zip.Reader
   113  // to the given destination.
   114  func (t *theme) writeFile(n string, d string) error {
   115  	verbose("attempting to write a file from zip: " + n)
   116  	f, err := t.a.Open(n)
   117  	if checkError(err) {
   118  		return err
   119  	}
   120  	fb, err := io.ReadAll(f)
   121  	if checkError(err) {
   122  		return err
   123  	}
   124  
   125  	g, err := os.Create(d)
   126  	if checkError(err) {
   127  		return err
   128  	}
   129  
   130  	_, err = g.Write(fb)
   131  	if checkError(err) {
   132  		return err
   133  	}
   134  
   135  	return nil
   136  }
   137  
   138  // writeDir writes the contents of a named directory in a
   139  // theme's zip.Reader into the named directory d, creating
   140  // a directory in the process. A list of files can be given
   141  // to writeDir, allowing it to skip checking the entire
   142  // directory.
   143  //
   144  // NOTE: I haven't implemented the directory read yet.
   145  // This is expected to be used in conjunction with
   146  // the theme.json setup!
   147  func (t *theme) writeDir(n string, d string, files ...string) error {
   148  	verbose("attempting to write a directory from zip: " + n)
   149  	i, err := t.a.Open(n)
   150  	if checkError(err) {
   151  		return err
   152  	}
   153  	in, err := i.Stat()
   154  	if checkError(err) {
   155  		return err
   156  	}
   157  
   158  	if !in.IsDir() {
   159  		return errors.New("not a valid directory")
   160  	}
   161  
   162  	if len(files) == 0 {
   163  		return errors.New("zip file directory checking not implemented yet")
   164  	}
   165  
   166  	if !fileCheck(filepath.Join(d, n)) {
   167  		err = os.Mkdir(filepath.Join(d, n), 0755)
   168  		if checkError(err) {
   169  			return err
   170  		}
   171  	}
   172  
   173  	for _, f := range files {
   174  		err := t.writeFile(filepath.Join(n, f), filepath.Join(d, n, f))
   175  		if checkError(err) {
   176  			return err
   177  		}
   178  	}
   179  
   180  	return nil
   181  }
   182  
   183  // copyTheme copies a theme to a destination string,
   184  // making a directory within d containing the entire
   185  // tree of the theme as described in theme.json,
   186  // as well as copying over theme.json for later use.
   187  func (t *theme) copyTheme(d string) error {
   188  
   189  	return nil
   190  }
   191  
   192  // webVars dictate where fotoDen gets its JavaScript and CSS files per page.
   193  // Four variables are definite - and BaseURL is the most important one.
   194  // PageVars indicate optional variables to be passed to the Go template engine.
   195  type webVars struct {
   196  	BaseURL     string
   197  	JSLocation  string
   198  	CSSLocation string
   199  	IsStatic    bool
   200  	PageVars    map[string]string
   201  }
   202  
   203  // newWebVars creates a WebVars object. Takes a single URL string, and outputs
   204  // a set of fotoDen compatible URLs.
   205  func newWebVars(u, folder string) (*webVars, error) {
   206  
   207  	webvars := new(webVars)
   208  	url, err := url.Parse(u)
   209  	jsurl, err := url.Parse(u)
   210  	cssurl, err := url.Parse(u)
   211  	if err != nil {
   212  		return webvars, err
   213  	}
   214  
   215  	webvars.BaseURL = url.String()
   216  	if len(webvars.BaseURL) > 0 && webvars.BaseURL[len(webvars.BaseURL)-1] == '/' {
   217  		webvars.BaseURL = webvars.BaseURL[0 : len(webvars.BaseURL)-1]
   218  	}
   219  
   220  	f := new(generator.Folder)
   221  	fpath, _ := filepath.Abs(folder)
   222  
   223  	err = f.ReadFolderInfo(filepath.Join(fpath, "folderInfo.json"))
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	superFolder, err := func() (string, error) {
   229  		f := new(generator.Folder)
   230  
   231  		_, err := os.Stat(filepath.Join(filepath.Dir(fpath), "folderInfo.json"))
   232  		if os.IsNotExist(err) {
   233  			return "", nil
   234  		} else if checkError(err) {
   235  			return "", err
   236  		}
   237  
   238  		verbose("Folder above is a fotoDen folder, using that...")
   239  		err = f.ReadFolderInfo(filepath.Join(filepath.Dir(fpath), "folderInfo.json"))
   240  		if checkError(err) {
   241  			return "", err
   242  		}
   243  
   244  		return f.Name, nil
   245  	}()
   246  
   247  	if checkError(err) {
   248  		verbose("Could not read folder above the current one")
   249  	}
   250  
   251  	jsurl.Path = path.Join(jsurl.Path, "js", "fotoDen.js")
   252  	webvars.JSLocation = jsurl.String()
   253  
   254  	cssurl.Path = path.Join(cssurl.Path, "css", "theme.css")
   255  	webvars.CSSLocation = cssurl.String()
   256  
   257  	if f.Static {
   258  		webvars.PageVars = map[string]string{
   259  			"name": f.Name,
   260  			"desc": f.Desc,
   261  			"sfol": superFolder,
   262  		}
   263  	}
   264  
   265  	return webvars, nil
   266  }
   267  
   268  type page int
   269  
   270  const (
   271  	photo page = iota
   272  	album
   273  	folder
   274  	info
   275  )
   276  
   277  // configurePage configures the various Go template
   278  // variables within a page according to a specific type.
   279  // u is the URL of a website,
   280  // d is the destination that the result goes into,
   281  // t is the type of page,
   282  // i is the set of variables to use
   283  func (t *theme) configurePage(u, d string, y page, i *webVars) error {
   284  	var f string
   285  	p := template.New("result")
   286  
   287  	r, err := os.Create(d)
   288  	if err != nil {
   289  		return err
   290  	}
   291  
   292  	switch y {
   293  	case photo:
   294  		f = t.f["photo-template.html"]
   295  	case album:
   296  		f = t.f["album-template.html"]
   297  	case folder:
   298  		f = t.f["folder-template.html"]
   299  	case info:
   300  		f = t.f["page-template.html"]
   301  	}
   302  
   303  	m, err := p.Parse(f)
   304  	if err != nil {
   305  		return err
   306  	}
   307  
   308  	return m.Execute(r, i)
   309  }
   310  
   311  // generateWeb takes a mode, a destinatination, and an optional map[string]string.
   312  // If i is not nil, that map will be merged into the WebVars PageVars field.
   313  func (t *theme) generateWeb(m, dest string, i map[string]string) error {
   314  	var err error
   315  	var v *webVars
   316  
   317  	if m == "folder" || m == "album" {
   318  		v, err = newWebVars(generator.CurrentConfig.WebBaseURL, dest)
   319  		if err != nil {
   320  			return err
   321  		}
   322  	} else {
   323  		v = new(webVars)
   324  		v.BaseURL = generator.CurrentConfig.WebBaseURL
   325  		v.PageVars = make(map[string]string)
   326  	}
   327  
   328  	if i != nil {
   329  		for k, a := range i {
   330  			v.PageVars[k] = a
   331  		}
   332  	}
   333  
   334  	switch m {
   335  	case "folder":
   336  		err = t.configurePage(generator.CurrentConfig.WebBaseURL, path.Join(dest, "index.html"), folder, v)
   337  		if checkError(err) {
   338  			return err
   339  		}
   340  	case "album":
   341  		err = t.configurePage(generator.CurrentConfig.WebBaseURL, path.Join(dest, "index.html"), album, v)
   342  		if checkError(err) {
   343  			return err
   344  		}
   345  
   346  		err = t.configurePage(generator.CurrentConfig.WebBaseURL, path.Join(dest, "photo.html"), photo, v)
   347  		if checkError(err) {
   348  			return err
   349  		}
   350  	case "page":
   351  		err = t.configurePage(generator.CurrentConfig.WebBaseURL, dest, info, v)
   352  
   353  		if checkError(err) {
   354  			return err
   355  		}
   356  	}
   357  
   358  	return nil
   359  }
   360  
   361  /// GLOBAL THEME VAR ///
   362  // this probably *could* be mitigated by
   363  // including the current theme's name in the
   364  // config, allowing for functions to refer to
   365  // the current *website*'s theme
   366  
   367  var currentTheme *theme
   368  
   369  func setCurrentTheme(t string) error {
   370  	c, err := os.UserConfigDir()
   371  	if checkError(err) {
   372  		return err
   373  	}
   374  
   375  	if t != "Default" {
   376  		p := filepath.Join(c, "fotoDen", "themes", t+".zip")
   377  		z, s, err := zipFileReader(p)
   378  		if checkError(err) {
   379  			return err
   380  		}
   381  
   382  		currentTheme, err = openTheme(z, s)
   383  		if checkError(err) {
   384  			return err
   385  		}
   386  	} else {
   387  		if isEmbed {
   388  			currentTheme, _ = openTheme(defaultThemeZipReader(), defaultThemeZipLen)
   389  		} else {
   390  			return fmt.Errorf("could not find a fotoDen theme to use")
   391  		}
   392  	}
   393  
   394  	return nil
   395  }
   396  
   397  func openDefaultTheme() error {
   398  	if fileCheck(path.Join(generator.RootConfigDir, "defaulttheme")) {
   399  		f, err := os.Open(path.Join(generator.RootConfigDir, "defaulttheme"))
   400  		d, err := ioutil.ReadAll(f)
   401  		if checkError(err) {
   402  			return err
   403  		}
   404  
   405  		err = setCurrentTheme(string(d))
   406  		if checkError(err) {
   407  			return err
   408  		}
   409  	} else {
   410  		return fmt.Errorf("warning: could not find a default fotoDen theme to use")
   411  	}
   412  
   413  	return nil
   414  }
   415  
   416  // initTheme initializes a theme to a site, replacing
   417  // all relevant variables according to what fotoDen
   418  // needs in order to generate a gallery.
   419  // u is the URL of the site
   420  // e is the template directory of the site
   421  // r is the root directory of the site
   422  func (t *theme) initTheme(u string, e string, r string) error {
   423  	var err error
   424  	verbose("attempting to initialize a theme")
   425  	/*m, err := os.MkdirTemp("", "")
   426  	if checkError(err) {
   427  		return err
   428  	}
   429  	verbose("temp dir created in: " + m)
   430  
   431  	wvars, err := NewWebVars(u)
   432  	wvars.PageVars["pageContent"] = "{{.PageContent}}" // hacky
   433  	checkError(err)
   434  
   435  	hf := []string{"photo-template.html", "album-template.html", "folder-template.html", "page-template.html"}
   436  
   437  	verbose("generating temporary template setup files")
   438  	for _, f := range hf {
   439  		err = t.writeFile(filepath.Join("html", f), filepath.Join(m, f))
   440  		if checkError(err) {
   441  			return err
   442  		}
   443  	}
   444  
   445  	verbose("writing HTML templates now to: " + e)
   446  	err = os.MkdirAll(filepath.Join(e, "html"), 0755)
   447  	if checkError(err) {
   448  		return err
   449  	}
   450  	for _, f := range hf {
   451  		err = ConfigureWebFile(
   452  			filepath.Join(m, f),
   453  			filepath.Join(e, "html", f),
   454  			wvars,
   455  		)
   456  		if checkError(err) {
   457  			return err
   458  		}
   459  	}*/
   460  
   461  	copyArray := func(f []string, n string) error {
   462  		if len(f) != 0 {
   463  			err = t.writeDir(n, r, f...)
   464  			if checkError(err) {
   465  				return err
   466  			}
   467  		}
   468  
   469  		return nil
   470  	}
   471  
   472  	verbose("copying over theme folders into: " + r)
   473  	err = copyArray(t.s.Stylesheets, "css")
   474  	err = copyArray(t.s.Scripts, "js")
   475  	err = copyArray(t.s.Other, "etc")
   476  
   477  	return err
   478  }