Initial commit
commit
2a081b0a2d
|
@ -0,0 +1,3 @@
|
||||||
|
lamium
|
||||||
|
lm
|
||||||
|
todo.txt
|
|
@ -0,0 +1,76 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
tpl "lamium/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// App information.
|
||||||
|
app = struct {
|
||||||
|
name string
|
||||||
|
cmd string
|
||||||
|
desc string
|
||||||
|
version string
|
||||||
|
configPath string
|
||||||
|
}{
|
||||||
|
name: "Lamium",
|
||||||
|
cmd: "lm",
|
||||||
|
desc: "Static web album generator",
|
||||||
|
version: "0.1.0",
|
||||||
|
configPath: "lamium.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command line menu options.
|
||||||
|
helpOpts = [][]string{
|
||||||
|
{"Usage: " + app.cmd + " [option]\n"},
|
||||||
|
{"Options:"},
|
||||||
|
{"-h", "help", "Show this help message"},
|
||||||
|
{"-m", "make", "Make album in the configured directory"},
|
||||||
|
{"-n", "new", "Place a new config in the current directory"},
|
||||||
|
{"-v", "version", "Show app version"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prints a help message to standard output.
|
||||||
|
func showHelp() {
|
||||||
|
for i := range helpOpts {
|
||||||
|
if len(helpOpts[i]) >= 3 {
|
||||||
|
fmt.Println(helpOpts[i][0]+", "+helpOpts[i][1], "\t\t"+
|
||||||
|
helpOpts[i][2])
|
||||||
|
} else {
|
||||||
|
fmt.Println(helpOpts[i][0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints version information.
|
||||||
|
func showVersion() {
|
||||||
|
fmt.Println(app.name, app.version, "—", app.desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps command line options to corresponding functions.
|
||||||
|
func parseArgs(args []string) {
|
||||||
|
if len(args) > 1 {
|
||||||
|
switch args[1] {
|
||||||
|
case "-h", "h", "help":
|
||||||
|
showHelp()
|
||||||
|
case "-m", "m", "make":
|
||||||
|
conf := loadConfig(app.configPath)
|
||||||
|
index := map[string]string{
|
||||||
|
"html": conf.IndexHTML,
|
||||||
|
"type": conf.IndexType,
|
||||||
|
"title": conf.IndexTitle,
|
||||||
|
}
|
||||||
|
tpl.MakeAlbum(conf.ThemeDir, conf.HTMLDir, index, conf.Sets)
|
||||||
|
case "-n", "n", "new":
|
||||||
|
genConfig(app.configPath)
|
||||||
|
tpl.GenTheme(false)
|
||||||
|
case "-v", "v", "version":
|
||||||
|
showVersion()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showHelp()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
tpl "lamium/template"
|
||||||
|
util "lamium/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// A Config defines settings for album generation.
|
||||||
|
Config struct {
|
||||||
|
HTMLDir string `json:"htmlDir"`
|
||||||
|
IndexHTML string `json:"indexHTML"`
|
||||||
|
IndexTitle string `json:"indexTitle"`
|
||||||
|
IndexType string `json:"indexType"`
|
||||||
|
ThemeDir string `json:"themeDir"`
|
||||||
|
Sets []tpl.Set `json:"sets"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Example configuration.
|
||||||
|
sampleConfig = Config{
|
||||||
|
HTMLDir: "public_html",
|
||||||
|
IndexHTML: "index.html",
|
||||||
|
IndexTitle: "My Album",
|
||||||
|
IndexType: "text",
|
||||||
|
ThemeDir: "themes/nettle",
|
||||||
|
Sets: []tpl.Set{
|
||||||
|
{
|
||||||
|
Name: "set1",
|
||||||
|
ImageDir: "set1",
|
||||||
|
IndexHTML: "index.html",
|
||||||
|
Title: "Set 1",
|
||||||
|
SortOrder: "modTimeDesc",
|
||||||
|
CSSClassIframe: "frame",
|
||||||
|
CSSClassLink: "link",
|
||||||
|
CSSClassThumb: "thumb",
|
||||||
|
LightboxOn: true,
|
||||||
|
LinkType: "page",
|
||||||
|
ThumbDir: "thumbs",
|
||||||
|
ThumbWidth: 200,
|
||||||
|
ThumbHeight: 200,
|
||||||
|
ThumbPct: 0,
|
||||||
|
ThumbClip: "square",
|
||||||
|
ThumbUnifyMode: "height",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error messages.
|
||||||
|
configErr = struct {
|
||||||
|
configNotLoaded string
|
||||||
|
jsonNotSaved string
|
||||||
|
}{
|
||||||
|
configNotLoaded: "Error: config could not be loaded:",
|
||||||
|
jsonNotSaved: "Error: JSON config could not be saved:",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// genConfig outputs an example configuration file at path.
|
||||||
|
func genConfig(path string) {
|
||||||
|
conf, jsonErr := json.MarshalIndent(sampleConfig, "", " ")
|
||||||
|
if !util.HasError(jsonErr, configErr.jsonNotSaved+" "+path, true) {
|
||||||
|
util.MakeDir(filepath.Dir(path))
|
||||||
|
util.SaveFile(path, string(conf), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig returns a *Config interface of settings after reading
|
||||||
|
// the configuration file at path.
|
||||||
|
func loadConfig(path string) *Config {
|
||||||
|
contents := util.LoadFile(path, true)
|
||||||
|
conf := &Config{}
|
||||||
|
json.Unmarshal([]byte(contents), &conf)
|
||||||
|
return conf
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||||
|
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
|
@ -0,0 +1,364 @@
|
||||||
|
// Package image uses the official Go image/draw library to provide more options // for image downscaling. It includes functions related to the supported image
|
||||||
|
// formats.
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/gif"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
|
||||||
|
util "lamium/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Error messages.
|
||||||
|
err = struct {
|
||||||
|
imageNotCreated string
|
||||||
|
imageNotLoaded string
|
||||||
|
imageNotDecoded string
|
||||||
|
imageNotEncoded string
|
||||||
|
}{
|
||||||
|
imageNotCreated: "Error: new image not created.",
|
||||||
|
imageNotLoaded: "Error: image could not be loaded:",
|
||||||
|
imageNotDecoded: "Error: image could not be decoded:",
|
||||||
|
imageNotEncoded: "Error: image could not be encoded:",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported image extensions.
|
||||||
|
imageExt = map[string][]string{
|
||||||
|
"gif": {".gif", ".GIF"},
|
||||||
|
"jpg": {".jpeg", ".jpg", ".JPEG", ".JPG"},
|
||||||
|
"png": {".png", ".PNG"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image information messages.
|
||||||
|
msg = struct {
|
||||||
|
debugSrcImg string
|
||||||
|
debugSrcPath string
|
||||||
|
debugNewImg string
|
||||||
|
imgResize string
|
||||||
|
}{
|
||||||
|
debugSrcImg: "Source image size:",
|
||||||
|
debugSrcPath: "Image:",
|
||||||
|
debugNewImg: "New image size:",
|
||||||
|
imgResize: "Resizing",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsSupported checks an image's file extension returns true if it is one of the // supported extensions.
|
||||||
|
func IsSupported(path string) bool {
|
||||||
|
var exts []string
|
||||||
|
for _, s := range imageExt {
|
||||||
|
for _, v := range s {
|
||||||
|
exts = append(exts, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Contains(exts, filepath.Ext(path)) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyImages copies image files from srcPath to destPath, overwriting files at
|
||||||
|
// the destination if overwrite is set to true.
|
||||||
|
func CopyImages(srcPath, destPath string, overwrite bool) {
|
||||||
|
files := util.GetFileList(srcPath, "name", false)
|
||||||
|
for _, file := range files {
|
||||||
|
destFile := filepath.Join(destPath, filepath.Base(file))
|
||||||
|
if (!util.FileExists(destFile) || (util.FileExists(destFile) &&
|
||||||
|
overwrite)) && IsSupported(file) {
|
||||||
|
util.CopyFile(file, destFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImageList returns a slice of strings of image files in srcPath.
|
||||||
|
//
|
||||||
|
// For sortOrder options, see util.GetFileList.
|
||||||
|
func GetImageList(srcPath, sortOrder string, includeDirs bool) []string {
|
||||||
|
files := util.GetFileList(srcPath, sortOrder, includeDirs)
|
||||||
|
var list []string
|
||||||
|
for _, file := range files {
|
||||||
|
if IsSupported(file) {
|
||||||
|
list = append(list, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeImage returns an image.Image object from an image path.
|
||||||
|
func DecodeImage(path string) image.Image {
|
||||||
|
var (
|
||||||
|
img image.Image
|
||||||
|
decErr error
|
||||||
|
)
|
||||||
|
srcFile, openErr := os.Open(filepath.FromSlash(path))
|
||||||
|
defer srcFile.Close()
|
||||||
|
if !util.HasError(openErr, err.imageNotLoaded+" "+path) {
|
||||||
|
if slices.Contains(imageExt["jpg"], filepath.Ext(path)) {
|
||||||
|
img, decErr = jpeg.Decode(srcFile)
|
||||||
|
} else if slices.Contains(imageExt["png"], filepath.Ext(path)) {
|
||||||
|
img, decErr = png.Decode(srcFile)
|
||||||
|
} else if slices.Contains(imageExt["gif"], filepath.Ext(path)) {
|
||||||
|
img, decErr = gif.Decode(srcFile)
|
||||||
|
} else {
|
||||||
|
util.HasError(decErr, err.imageNotDecoded+" "+path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
util.HasError(decErr, err.imageNotDecoded+" "+path)
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeImage writes an image.Image object as image file path.
|
||||||
|
func EncodeImage(path string, writer io.Writer, img image.Image) {
|
||||||
|
var encErr error
|
||||||
|
if slices.Contains(imageExt["jpg"], filepath.Ext(path)) {
|
||||||
|
encErr = jpeg.Encode(writer, img, nil)
|
||||||
|
} else if slices.Contains(imageExt["png"], filepath.Ext(path)) {
|
||||||
|
encErr = png.Encode(writer, img)
|
||||||
|
} else if slices.Contains(imageExt["gif"], filepath.Ext(path)) {
|
||||||
|
encErr = gif.Encode(writer, img, nil)
|
||||||
|
} else {
|
||||||
|
util.HasError(encErr, err.imageNotEncoded+" "+path)
|
||||||
|
}
|
||||||
|
util.HasError(encErr, err.imageNotEncoded+" "+path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalcImageSize returns the new width and height of image.Image srcImg
|
||||||
|
// based on pct, clip and unifyMode.
|
||||||
|
//
|
||||||
|
// pct is percentage of the original size, an int value between 1-99
|
||||||
|
// inclusively.
|
||||||
|
//
|
||||||
|
// clip options: square, rect.
|
||||||
|
//
|
||||||
|
// unifyMode options with example scenarios if width is 200 and height is 100:
|
||||||
|
//
|
||||||
|
// maxSize: images are the maximum size at the longer side.
|
||||||
|
// An image in landscape would be 200px in width and less in height.
|
||||||
|
// An image in portrait would have a height of 200px, less in width.
|
||||||
|
// A square image would have a width and height of 200px.
|
||||||
|
//
|
||||||
|
// minSize: images are the minimum size at the shorter side.
|
||||||
|
// An image in landscape would be 100px in height and more in width.
|
||||||
|
// An image in portrait would have a width of 100px, more in height.
|
||||||
|
// A square image would have a width and height of 100px.
|
||||||
|
//
|
||||||
|
// width: images have the same width.
|
||||||
|
// An image in landscape would be 200px wide, less in height.
|
||||||
|
// Animage in portrait would be 200px wide, more in height
|
||||||
|
// A square image would have a width and height of 200px.
|
||||||
|
//
|
||||||
|
// height: images have the same height.
|
||||||
|
// An image in landscape would be 100px high and wider than 100px.
|
||||||
|
// An image in portrait would be 100px high, and narrower than 100px.
|
||||||
|
// A square image would have a width and height of 100px.
|
||||||
|
//
|
||||||
|
// none: images are sized at a percentage of the original image regardless of
|
||||||
|
// set width and height. The current default is 50% of the original image
|
||||||
|
// dimensions, e.g. a 600 x 800px source image yields a new 300 x 400px image.
|
||||||
|
func CalcImageSize(srcImg image.Image, width, height, pct int,
|
||||||
|
clip string, unifyMode string) (int, int) {
|
||||||
|
// Constrain width and height to source dimensions.
|
||||||
|
w, h := width, height
|
||||||
|
if w > srcImg.Bounds().Dx() || w < 0 {
|
||||||
|
w = srcImg.Bounds().Dx()
|
||||||
|
}
|
||||||
|
if h > srcImg.Bounds().Dy() || h < 0 {
|
||||||
|
h = srcImg.Bounds().Dy()
|
||||||
|
}
|
||||||
|
// Get source aspect ratio.
|
||||||
|
minSrcSize, maxSrcSize := util.MinMax(srcImg.Bounds().Dx(),
|
||||||
|
srcImg.Bounds().Dy())
|
||||||
|
var ratio = float64(minSrcSize) / float64(maxSrcSize)
|
||||||
|
switch unifyMode {
|
||||||
|
case "maxSize":
|
||||||
|
// Use the higher value of width/height as maximum size.
|
||||||
|
_, maxSize := util.MinMax(w, h)
|
||||||
|
if srcImg.Bounds().Dx() > srcImg.Bounds().Dy() {
|
||||||
|
w = maxSize
|
||||||
|
h = int(math.Round(float64(w) * ratio))
|
||||||
|
} else if srcImg.Bounds().Dy() > srcImg.Bounds().Dx() {
|
||||||
|
w = int(math.Round(float64(maxSize) * ratio))
|
||||||
|
h = maxSize
|
||||||
|
} else {
|
||||||
|
w, h = maxSize, maxSize
|
||||||
|
}
|
||||||
|
case "minSize":
|
||||||
|
// Use the lower value of width/height as minimum size.
|
||||||
|
minSize, _ := util.MinMax(w, h)
|
||||||
|
if srcImg.Bounds().Dx() > srcImg.Bounds().Dy() {
|
||||||
|
w = int(math.Round(float64(srcImg.Bounds().Dx()) /
|
||||||
|
(float64(srcImg.Bounds().Dy()) / float64(h))))
|
||||||
|
h = minSize
|
||||||
|
} else if srcImg.Bounds().Dx() < srcImg.Bounds().Dy() {
|
||||||
|
w = minSize
|
||||||
|
h = int(math.Round(float64(srcImg.Bounds().Dy()) /
|
||||||
|
(float64(srcImg.Bounds().Dx()) / float64(w))))
|
||||||
|
} else {
|
||||||
|
w, h = minSize, minSize
|
||||||
|
}
|
||||||
|
case "width":
|
||||||
|
// Use width as baseline for aspect ratio.
|
||||||
|
if srcImg.Bounds().Dx() != srcImg.Bounds().Dy() {
|
||||||
|
h = int(math.Round(float64(srcImg.Bounds().Dy()) /
|
||||||
|
(float64(srcImg.Bounds().Dx()) / float64(w))))
|
||||||
|
} else {
|
||||||
|
h = w
|
||||||
|
}
|
||||||
|
case "height":
|
||||||
|
// Use height as baseline for aspect ratio.
|
||||||
|
if srcImg.Bounds().Dx() != srcImg.Bounds().Dy() {
|
||||||
|
w = int(math.Round(float64(srcImg.Bounds().Dx()) /
|
||||||
|
(float64(srcImg.Bounds().Dy()) / float64(h))))
|
||||||
|
} else {
|
||||||
|
w = h
|
||||||
|
}
|
||||||
|
case "none":
|
||||||
|
// Derive values as a percentage of source image size.
|
||||||
|
pc := pct
|
||||||
|
if pct > 0 || pct < 100 {
|
||||||
|
pc = 50
|
||||||
|
}
|
||||||
|
if srcImg.Bounds().Dx() > srcImg.Bounds().Dy() {
|
||||||
|
w = int(math.Round(float64(srcImg.Bounds().Dx()) *
|
||||||
|
(float64(pc) / 100)))
|
||||||
|
h = int(math.Round(float64(srcImg.Bounds().Dx()) * ratio *
|
||||||
|
(float64(pc) / 100)))
|
||||||
|
} else {
|
||||||
|
w = int(math.Round(float64(srcImg.Bounds().Dy()) * ratio *
|
||||||
|
(float64(pc) / 100)))
|
||||||
|
h = int(math.Round(float64(srcImg.Bounds().Dy()) *
|
||||||
|
(float64(pc) / 100)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if clip == "square" {
|
||||||
|
switch unifyMode {
|
||||||
|
case "minSize":
|
||||||
|
w, _ = util.MinMax(w, h)
|
||||||
|
h = w
|
||||||
|
case "maxSize", "none":
|
||||||
|
_, w = util.MinMax(w, h)
|
||||||
|
h = w
|
||||||
|
case "width":
|
||||||
|
h = w
|
||||||
|
case "height":
|
||||||
|
w = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return w, h
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalcClipSize returns the new width and height as well as an image.Rectangle
|
||||||
|
// object for image.Image srcImg based on a set width and height and clip mode
|
||||||
|
// (square or rect).
|
||||||
|
func CalcClipSize(srcImg image.Image, width, height int, clip string) (int,
|
||||||
|
int, image.Rectangle) {
|
||||||
|
var w, h int
|
||||||
|
var srcBounds image.Rectangle
|
||||||
|
if clip == "square" {
|
||||||
|
// Center the square on rectangular images.
|
||||||
|
// Use max length for new image size if width and height are
|
||||||
|
// different.
|
||||||
|
_, w = util.MinMax(width, height)
|
||||||
|
h = w
|
||||||
|
// Get the shorter side.
|
||||||
|
srcMin, _ := util.MinMax(srcImg.Bounds().Dx(), srcImg.Bounds().Dy())
|
||||||
|
x1, y1 := 0, 0
|
||||||
|
x2, y2 := srcMin, srcMin
|
||||||
|
// Shift the start and end values on the longer side of the
|
||||||
|
// source image's image.Rectangle.
|
||||||
|
if srcImg.Bounds().Dx() != srcImg.Bounds().Dy() &&
|
||||||
|
srcMin == srcImg.Bounds().Dx() {
|
||||||
|
y1 = int(math.Round(float64(srcImg.Bounds().Dy()-
|
||||||
|
srcMin) / float64(2)))
|
||||||
|
y2 = int(srcMin + y1)
|
||||||
|
} else if srcImg.Bounds().Dx() != srcImg.Bounds().Dy() &&
|
||||||
|
srcMin == srcImg.Bounds().Dy() {
|
||||||
|
x1 = int(math.Round(float64(srcImg.Bounds().Dx()-
|
||||||
|
srcMin) / float64(2)))
|
||||||
|
x2 = int(srcMin + x1)
|
||||||
|
}
|
||||||
|
srcBounds = image.Rect(x1, y1, x2, y2)
|
||||||
|
} else {
|
||||||
|
w, h = width, height
|
||||||
|
srcBounds = srcImg.Bounds()
|
||||||
|
}
|
||||||
|
return w, h, srcBounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizeImage creates a new downscaled image at destPath from source image
|
||||||
|
// srcPath.
|
||||||
|
//
|
||||||
|
// For more details about the pct, clip, and unifyMode parameters, see
|
||||||
|
// CalcImageSize.
|
||||||
|
func ResizeImage(srcPath, destPath string, width, height, pct int, clip,
|
||||||
|
unifyMode string, debug ...bool) {
|
||||||
|
util.MakeDir(filepath.Dir(destPath), true)
|
||||||
|
var destFile *os.File
|
||||||
|
var createErr error
|
||||||
|
if IsSupported(srcPath) {
|
||||||
|
destFile, createErr = os.Create(filepath.FromSlash(destPath))
|
||||||
|
defer destFile.Close()
|
||||||
|
}
|
||||||
|
if !util.HasError(createErr, err.imageNotCreated) && IsSupported(srcPath) {
|
||||||
|
w, h := width, height
|
||||||
|
var srcBounds image.Rectangle
|
||||||
|
srcImg := DecodeImage(srcPath)
|
||||||
|
w, h = CalcImageSize(srcImg, w, h, pct, clip, unifyMode)
|
||||||
|
w, h, srcBounds = CalcClipSize(srcImg, w, h, clip)
|
||||||
|
|
||||||
|
destImg := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
|
draw.BiLinear.Scale(destImg, destImg.Rect, srcImg, srcBounds, draw.Over,
|
||||||
|
nil)
|
||||||
|
EncodeImage(destPath, destFile, destImg)
|
||||||
|
|
||||||
|
if slices.Contains(debug, true) {
|
||||||
|
fmt.Println(msg.debugSrcPath, srcPath)
|
||||||
|
fmt.Println(msg.debugSrcImg, srcImg.Bounds().Dx(),
|
||||||
|
srcImg.Bounds().Dy())
|
||||||
|
fmt.Println(msg.debugNewImg, w, h)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizeImages creates multiple downscaled images at directory destPath from
|
||||||
|
// source images at directory srcPath.
|
||||||
|
func ResizeImages(srcPath, destPath string, width, height, pct int, clip string,
|
||||||
|
unifyMode string, overwrite bool, debug ...bool) {
|
||||||
|
files := util.GetFileList(srcPath, "name", false)
|
||||||
|
debugOn := false
|
||||||
|
if slices.Contains(debug, true) {
|
||||||
|
debugOn = true
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
destImg := filepath.Join(destPath, filepath.Base(file))
|
||||||
|
if !util.FileExists(destImg) || (util.FileExists(destImg) &&
|
||||||
|
overwrite) {
|
||||||
|
ResizeImage(file, destImg, width, height, pct, clip, unifyMode,
|
||||||
|
debugOn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImageTitle returns a title string based on the image filename in path.
|
||||||
|
func GetImageTitle(path string) string {
|
||||||
|
repChars := map[string]string{"-": " ", "_": " "}
|
||||||
|
str := strings.Replace(util.MultiReplace(filepath.Base(path), repChars),
|
||||||
|
filepath.Ext(path), "", 1)
|
||||||
|
return str
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
parseArgs(os.Args)
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Lamium
|
||||||
|
|
||||||
|
A simple HTML album generator.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- Generate rectangular or square image thumbnails
|
||||||
|
- Default plain theme with CSS lightbox
|
||||||
|
- Supported image types: .jpg, .png
|
||||||
|
- Supported image caption files: .txt, .html
|
||||||
|
|
||||||
|
|
||||||
|
*This is currently a preview release and may contain issues.*
|
||||||
|
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
- Download the source or `git clone` the repo.
|
||||||
|
|
||||||
|
- Install [Go](https://golang.org), then build: `go build -o lm .`
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- Make a new album directory and change into it: `mkdir [album] && cd [album]`
|
||||||
|
|
||||||
|
- Add a folder of images (a "set") to the directory, including any image captions as .txt files.
|
||||||
|
|
||||||
|
- Generate a new sample config and theme: `lm new`
|
||||||
|
|
||||||
|
- Edit the settings in the `.json` config file.
|
||||||
|
|
||||||
|
- Customise the default theme templates as desired.
|
||||||
|
|
||||||
|
- Generate the album: `lm make`
|
||||||
|
|
||||||
|
By default the `make` option will output a `public_html` folder inside the album source directory with the generated HTML files, images and thumbnails. The HTML album can then be uploaded to a web server directory using rsync or another file transfer application.
|
||||||
|
|
||||||
|
|
||||||
|
Example album structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
album/
|
||||||
|
|- set1/
|
||||||
|
| |- image1.jpg
|
||||||
|
| |- image1.txt
|
||||||
|
| |- image2.jpg
|
||||||
|
| |- image2.txt
|
||||||
|
| |- index.txt
|
||||||
|
|- themes/
|
||||||
|
| |- [theme]/
|
||||||
|
| |- index.txt
|
||||||
|
|- lamium.json
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[0BSD](https://spdx.org/licenses/0BSD.html)
|
|
@ -0,0 +1,658 @@
|
||||||
|
// Package template implements HTML page generation functions from a default
|
||||||
|
// or custom theme.
|
||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"html"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
image "lamium/image"
|
||||||
|
util "lamium/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// A Set is a collection of images. The Set type contains properties
|
||||||
|
// about the collection such as the source directory for the images
|
||||||
|
// and how to display them.
|
||||||
|
//
|
||||||
|
// JSON tags are included for config file marshalling.
|
||||||
|
Set struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ImageDir string `json:"imageDir"`
|
||||||
|
IndexHTML string `json:"indexHTML"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
SortOrder string `json:"sortOrder"`
|
||||||
|
CSSClassIframe string `json:"cssClassIframe"`
|
||||||
|
CSSClassLink string `json:"cssClassLink"`
|
||||||
|
CSSClassThumb string `json:"cssClassThumb"`
|
||||||
|
LightboxOn bool `json:"lightboxOn"`
|
||||||
|
LinkType string `json:"linkType"`
|
||||||
|
ThumbDir string `json:"thumbDir"`
|
||||||
|
ThumbWidth int `json:"thumbWidth"`
|
||||||
|
ThumbHeight int `json:"thumbHeight"`
|
||||||
|
ThumbPct int `json:"thumbPercent"`
|
||||||
|
ThumbClip string `json:"thumbClip"`
|
||||||
|
ThumbUnifyMode string `json:"thumbUnifyMode"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// File extensions for captions.
|
||||||
|
CaptionExt = map[string]string{
|
||||||
|
"html": ".html",
|
||||||
|
"txt": ".txt",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported HTML markup tags.
|
||||||
|
MarkupHTML = map[string]string{
|
||||||
|
"<b>": "<b>",
|
||||||
|
"</b>": "</b>",
|
||||||
|
"<blockquote>": "<blockquote>",
|
||||||
|
"</blockquote>": "</blockquote>",
|
||||||
|
"<br>": "<br>",
|
||||||
|
"<code>": "<code>",
|
||||||
|
"</code>": "</code>",
|
||||||
|
"<del>": "<del>",
|
||||||
|
"</del>": "</del>",
|
||||||
|
"<dd>": "<dd>",
|
||||||
|
"</dd>": "</dd>",
|
||||||
|
"<dl>": "<dl>",
|
||||||
|
"</dl>": "</dl>",
|
||||||
|
"<dt>": "<dt>",
|
||||||
|
"</dt>": "</dt>",
|
||||||
|
"<em>": "<em>",
|
||||||
|
"</em>": "</em>",
|
||||||
|
"<h1>": "<h1>",
|
||||||
|
"</h1>": "</h1>",
|
||||||
|
"<h2>": "<h2>",
|
||||||
|
"</h2>": "</h2>",
|
||||||
|
"<h3>": "<h3>",
|
||||||
|
"</h3>": "</h3>",
|
||||||
|
"<i>": "<i>",
|
||||||
|
"</i>": "</i>",
|
||||||
|
"<ol>": "<ol>",
|
||||||
|
"</ol>": "</ol>",
|
||||||
|
"<p>": "<p>",
|
||||||
|
"</p>": "</p>",
|
||||||
|
"<strong>": "<strong>",
|
||||||
|
"</strong>": "</strong>",
|
||||||
|
"<s>": "<s>",
|
||||||
|
"</s>": "</s>",
|
||||||
|
"<u>": "<u>",
|
||||||
|
"</u>": "</u>",
|
||||||
|
"<ul>": "<ul>",
|
||||||
|
"</ul>": "</ul>",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported markup syntax regexp for text files and the HTML tag
|
||||||
|
// translations.
|
||||||
|
MarkupTxt = []map[string]string{
|
||||||
|
{`^\*\*`: "<strong>"},
|
||||||
|
{`\*\*$`: "</strong>"},
|
||||||
|
{`\*\*\n`: "</strong>\n"},
|
||||||
|
{`\t\*\*`: "\t<strong>"},
|
||||||
|
{`\*\*\t`: "</strong>\t"},
|
||||||
|
{`[[:space:]]\*\*`: " <strong>"},
|
||||||
|
{`\*\*[[:space:]]`: "</strong> "},
|
||||||
|
{`^\*`: "<em>"},
|
||||||
|
{`\*$`: "</em>"},
|
||||||
|
{`\*\n`: "</em>\n"},
|
||||||
|
{`\t\*`: "\t<em>"},
|
||||||
|
{`\*\t`: "</em>\t"},
|
||||||
|
{`[[:space:]]\*`: " <em>"},
|
||||||
|
{`\*[[:space:]]`: "</em> "},
|
||||||
|
{`^__`: "<u>"},
|
||||||
|
{`__$`: "</u>"},
|
||||||
|
{`__\n`: "</u>\n"},
|
||||||
|
{`\t__`: "\t<u>"},
|
||||||
|
{`__\t`: "</u>\t"},
|
||||||
|
{`[[:space:]]__`: " <u>"},
|
||||||
|
{`__[[:space:]]`: "</u> "},
|
||||||
|
{`^\~\~`: "<s>"},
|
||||||
|
{`\~\~$`: "</s>"},
|
||||||
|
{`\~\~\n`: "</s>\n"},
|
||||||
|
{`\t\~\~`: "\t<s>"},
|
||||||
|
{`\~\~\t`: "</s>\t"},
|
||||||
|
{`[[:space:]]\~\~`: " <s>"},
|
||||||
|
{`\~\~[[:space:]]`: "</s> "},
|
||||||
|
{"\n\n": "</p>\n<p>"},
|
||||||
|
{" \n": "<br>"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template variables used in partials.
|
||||||
|
PartVar = map[string]string{
|
||||||
|
"id": "{{ id }}",
|
||||||
|
"iframeClass": "{{ iframe_class }}",
|
||||||
|
"link": "{{ link }}",
|
||||||
|
"linkClass": "{{ link_class }}",
|
||||||
|
"navNext": "{{ nav_next }}",
|
||||||
|
"navPrev": "{{ nav_prev }}",
|
||||||
|
"navTop": "{{ nav_top }}",
|
||||||
|
"thumb": "{{ thumb }}",
|
||||||
|
"thumbClass": "{{ thumb_class }}",
|
||||||
|
"set": "[set]",
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML partials that are inserted into templates.
|
||||||
|
PartHTML = map[string]string{
|
||||||
|
"img": `<a class="` + PartVar["linkClass"] + `" href="` +
|
||||||
|
PartVar["link"] + `"><img class="` + PartVar["thumbClass"] +
|
||||||
|
`" src="` + PartVar["thumb"] + `"></a>` + "\n",
|
||||||
|
"lightbox": `<iframe class="` + PartVar["iframeClass"] + `" id="` +
|
||||||
|
PartVar["id"] + `" src="` + PartVar["link"] + `"></iframe>
|
||||||
|
<nav class="nav">
|
||||||
|
<a class="prev" href="` + PartVar["navPrev"] + `">←</a>
|
||||||
|
<a class="close" href="#">☓</a>
|
||||||
|
<a class="next" href="` + PartVar["navNext"] + `">→</a>` +
|
||||||
|
`</nav>` + "\n",
|
||||||
|
"nav": `<a class="link" href="` + PartVar["navPrev"] + `">←</a>
|
||||||
|
<a class="link" href="` + PartVar["navTop"] + `">↑</a>
|
||||||
|
<a class="link" href="` + PartVar["navNext"] + `">→</a>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample theme directory path relative to the album root.
|
||||||
|
//
|
||||||
|
// If changing this value, the go:embed directive for the ThemeFile variable
|
||||||
|
// also needs to be updated, to store the theme files at compile time.
|
||||||
|
SampleThemeDir = "themes/nettle"
|
||||||
|
|
||||||
|
// Theme template filenames.
|
||||||
|
Theme = struct {
|
||||||
|
Index string `json:"index"`
|
||||||
|
Image string `json:"_image"`
|
||||||
|
Set string `json:"_set"`
|
||||||
|
Sets string `json:"_sets"`
|
||||||
|
Text string `json:"_text"`
|
||||||
|
AlbumCSS string `json:"albumCSS"`
|
||||||
|
LightboxCSS string `json:"lightboxCSS"`
|
||||||
|
}{
|
||||||
|
Index: "index.html",
|
||||||
|
Image: "_image.html",
|
||||||
|
Set: "_set.html",
|
||||||
|
Sets: "_sets.html",
|
||||||
|
Text: "_text.html",
|
||||||
|
AlbumCSS: "album.css",
|
||||||
|
LightboxCSS: "lightbox.css",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template variables used in theme files.
|
||||||
|
ThemeVar = map[string]map[string]string{
|
||||||
|
Theme.Index: {
|
||||||
|
"content": "{{ index_content }}",
|
||||||
|
"relPath": "{{ index_relpath }}",
|
||||||
|
"title": "{{ index_title }}",
|
||||||
|
},
|
||||||
|
Theme.Image: {
|
||||||
|
"caption": "{{ image_caption }}",
|
||||||
|
"path": "{{ image_path }}",
|
||||||
|
"title": "{{ image_title }}",
|
||||||
|
"nav": "{{ nav }}",
|
||||||
|
},
|
||||||
|
Theme.Set: {
|
||||||
|
"content": "{{ set }}",
|
||||||
|
"desc": "{{ set_desc }}",
|
||||||
|
"lightbox": "{{ set_lightbox }}",
|
||||||
|
"name": "{{ set_name }}",
|
||||||
|
"title": "{{ set_title }}",
|
||||||
|
},
|
||||||
|
Theme.Sets: {
|
||||||
|
"content": "{{ set_[set] }}",
|
||||||
|
"desc": "{{ set_[set]_desc }}",
|
||||||
|
"lightbox": "{{ set_[set]_lightbox }}",
|
||||||
|
"name": "{{ set_[set]_name }}",
|
||||||
|
"title": "{{ set_[set]_title }}",
|
||||||
|
},
|
||||||
|
Theme.Text: {
|
||||||
|
"content": "{{ text_content }}",
|
||||||
|
"title": "{{ text_title }}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Compile-time theme embed directive.
|
||||||
|
// Ensure this path corresponds with SampleThemeDir.
|
||||||
|
//
|
||||||
|
//go:embed themes/nettle/*
|
||||||
|
var ThemeFile embed.FS
|
||||||
|
|
||||||
|
// GetHTMLFilename returns the value of path with the base filename reset to
|
||||||
|
// Theme.Index if path contains a conflicting or invalid filename. Otherwise,
|
||||||
|
// returns a copy of path.
|
||||||
|
func GetHTMLFilename(path string) string {
|
||||||
|
str := filepath.Join(filepath.Dir(path), Theme.Index)
|
||||||
|
if util.ReplaceExt(filepath.Base(path), "") != "" && filepath.Ext(path) ==
|
||||||
|
filepath.Ext(Theme.Index) {
|
||||||
|
str = path
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDup returns a renamed path if a file by the same name already
|
||||||
|
// exists in the same srcDir and is not a caption text file. If the file path
|
||||||
|
// does not already exist, returns the original path.
|
||||||
|
func CheckDup(path, srcDir, ext string) string {
|
||||||
|
files := util.GetFileList(srcDir, "name", false)
|
||||||
|
name := util.ReplaceExt(filepath.Base(path), "")
|
||||||
|
num := strconv.Itoa(len(files) + 1)
|
||||||
|
newPath := filepath.Join(filepath.Dir(path), name+num+ext)
|
||||||
|
for _, file := range files {
|
||||||
|
if util.ReplaceExt(filepath.Base(file), "") == name &&
|
||||||
|
filepath.Base(file) != name+CaptionExt["txt"] {
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenTheme outputs the theme files to the album directory at the
|
||||||
|
// SampleThemeDir path.
|
||||||
|
func GenTheme(exitOnErr bool) {
|
||||||
|
util.MakeDir(SampleThemeDir, exitOnErr)
|
||||||
|
files := []string{Theme.Index, Theme.Image, Theme.Set, Theme.Sets,
|
||||||
|
Theme.Text, Theme.AlbumCSS, Theme.LightboxCSS}
|
||||||
|
for _, f := range files {
|
||||||
|
content, _ := ThemeFile.ReadFile(filepath.Join(SampleThemeDir, f))
|
||||||
|
util.SaveFile(filepath.Join(SampleThemeDir, f), string(content),
|
||||||
|
exitOnErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSetHTML returns an HTML string of image thumbnails for an image set.
|
||||||
|
//
|
||||||
|
// linkClass and thumbClass are CSS classes to assign to the links and
|
||||||
|
// thumbnails respectively for styling.
|
||||||
|
//
|
||||||
|
// If imageType is "image", the HTML target links will link directly to
|
||||||
|
// images instead of their corresponding HTML pages.
|
||||||
|
//
|
||||||
|
// If lightboxOn is true, the HTML target links will be set to anchors
|
||||||
|
// that are image iframe ids.
|
||||||
|
func GetSetHTML(srcPath, thumbPath, sortOrder, linkType, linkClass,
|
||||||
|
thumbClass string, lightboxOn, isTopLevel bool) string {
|
||||||
|
var htmlStr string
|
||||||
|
files := image.GetImageList(srcPath, sortOrder, false)
|
||||||
|
imgVars := map[string]string{
|
||||||
|
PartVar["linkClass"]: linkClass,
|
||||||
|
PartVar["thumbClass"]: thumbClass,
|
||||||
|
}
|
||||||
|
if linkType == "image" {
|
||||||
|
for _, file := range files {
|
||||||
|
// Adjust path for top-level page.
|
||||||
|
if isTopLevel {
|
||||||
|
imgVars[PartVar["link"]] = file
|
||||||
|
} else {
|
||||||
|
imgVars[PartVar["link"]] = filepath.Base(file)
|
||||||
|
}
|
||||||
|
imgVars[PartVar["thumb"]] = filepath.Join(thumbPath,
|
||||||
|
filepath.Base(file))
|
||||||
|
htmlStr += util.MultiReplace(PartHTML["img"], imgVars)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, file := range files {
|
||||||
|
if lightboxOn {
|
||||||
|
imgVars[PartVar["link"]] = "#" +
|
||||||
|
util.ReplaceExt(filepath.Base(file), "")
|
||||||
|
} else if !lightboxOn && isTopLevel {
|
||||||
|
imgVars[PartVar["link"]] = strings.Replace(file,
|
||||||
|
filepath.Ext(file), filepath.Ext(Theme.Index), 1)
|
||||||
|
} else {
|
||||||
|
imgVars[PartVar["link"]] = filepath.Base(
|
||||||
|
strings.Replace(file,
|
||||||
|
filepath.Ext(file), filepath.Ext(Theme.Index), 1))
|
||||||
|
}
|
||||||
|
imgVars[PartVar["thumb"]] = filepath.Join(thumbPath,
|
||||||
|
filepath.Base(file))
|
||||||
|
htmlStr += util.MultiReplace(PartHTML["img"], imgVars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimRight(htmlStr, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSetLBHTML returns an HTML string of image iframes and navigation links for // an image set.
|
||||||
|
//
|
||||||
|
// iframeClass is a CSS class to assign to the iframes for styling.
|
||||||
|
func GetSetLBHTML(srcPath, sortOrder, iframeClass string,
|
||||||
|
isTopLevel bool) string {
|
||||||
|
var htmlStr string
|
||||||
|
files := image.GetImageList(srcPath, sortOrder, false)
|
||||||
|
lbVars := map[string]string{
|
||||||
|
PartVar["iframeClass"]: iframeClass,
|
||||||
|
}
|
||||||
|
for f := 0; f < len(files); f++ {
|
||||||
|
prev, next := "#", "#"
|
||||||
|
if len(files) > 1 {
|
||||||
|
if f == 0 {
|
||||||
|
next = "#" + util.ReplaceExt(filepath.Base(files[f+1]), "")
|
||||||
|
} else if f == (len(files) - 1) {
|
||||||
|
prev = "#" + util.ReplaceExt(filepath.Base(files[f-1]), "")
|
||||||
|
next = "#"
|
||||||
|
} else {
|
||||||
|
prev = "#" + util.ReplaceExt(filepath.Base(files[f-1]), "")
|
||||||
|
next = "#" + util.ReplaceExt(filepath.Base(files[f+1]), "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lbVars[PartVar["navNext"]] = next
|
||||||
|
lbVars[PartVar["navPrev"]] = prev
|
||||||
|
lbVars[PartVar["id"]] = util.ReplaceExt(filepath.Base(files[f]), "")
|
||||||
|
if isTopLevel {
|
||||||
|
lbVars[PartVar["link"]] = strings.Replace(files[f],
|
||||||
|
filepath.Ext(files[f]), filepath.Ext(Theme.Index), 1)
|
||||||
|
} else {
|
||||||
|
lbVars[PartVar["link"]] = filepath.Base(strings.Replace(
|
||||||
|
files[f], filepath.Ext(files[f]), filepath.Ext(Theme.Index), 1))
|
||||||
|
}
|
||||||
|
htmlStr += util.MultiReplace(PartHTML["lightbox"], lbVars)
|
||||||
|
}
|
||||||
|
return strings.TrimRight(htmlStr, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxtToHTML returns an HTML string with supported markup syntax in string str
|
||||||
|
// replaced with HTML tags.
|
||||||
|
func TxtToHTML(str string) string {
|
||||||
|
htmlStr := html.EscapeString(str)
|
||||||
|
for _, r := range MarkupTxt {
|
||||||
|
for reg, repl := range r {
|
||||||
|
re, _ := regexp.Compile(reg)
|
||||||
|
htmlStr = string(re.ReplaceAll([]byte(htmlStr), []byte(repl)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
htmlStr = "<p>" + strings.TrimRight(htmlStr, "\n") + "</p>"
|
||||||
|
return htmlStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterHTML returns an HTML string with some characters escaped
|
||||||
|
// (see html.EscapeString) and supported markup tags exempted.
|
||||||
|
func FilterHTML(str string) string {
|
||||||
|
htmlStr := html.EscapeString(str)
|
||||||
|
htmlStr = strings.TrimRight(util.MultiReplace(htmlStr, MarkupHTML), "\n")
|
||||||
|
return htmlStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCaption returns an HTML string with the contents of an image caption for
|
||||||
|
// string path, if the caption file exists. If the caption file does not exist,
|
||||||
|
// returns an empty string.
|
||||||
|
func GetCaption(path string) string {
|
||||||
|
for _, ext := range CaptionExt {
|
||||||
|
capPath := path
|
||||||
|
if strings.HasSuffix(path, ext) {
|
||||||
|
// Files.
|
||||||
|
capPath = strings.Replace(path, filepath.Ext(path), ext, 1)
|
||||||
|
} else {
|
||||||
|
// Directories.
|
||||||
|
capPath += ext
|
||||||
|
}
|
||||||
|
if util.FileExists(capPath) {
|
||||||
|
if ext == ".txt" {
|
||||||
|
return TxtToHTML(util.LoadFile(capPath))
|
||||||
|
} else {
|
||||||
|
return FilterHTML(util.LoadFile(capPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like GetCaption but for multiple files in a directory at string path,
|
||||||
|
// GetCaptions a map with image files as keys and caption contents as values.
|
||||||
|
//
|
||||||
|
// If titleFallback is true, sets the caption contents to the image title
|
||||||
|
// if no caption file is found for a given image.
|
||||||
|
func GetCaptions(path string, titleFallback bool) map[string]string {
|
||||||
|
caps := make(map[string]string)
|
||||||
|
files := image.GetImageList(path, "name", false)
|
||||||
|
// Load caption file if available, fallback to deriving title from the
|
||||||
|
// filename if title fallback is enabled, or empty string if unknown.
|
||||||
|
for _, file := range files {
|
||||||
|
caps[file] = GetCaption(file)
|
||||||
|
if caps[file] == "" && titleFallback {
|
||||||
|
caps[file] = image.GetImageTitle(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return caps
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyCSS copies the theme CSS files from themePath to destDir.
|
||||||
|
func CopyCSS(themePath, destDir string) {
|
||||||
|
util.MakeDir(destDir, true)
|
||||||
|
util.CopyFile(filepath.Join(themePath, Theme.LightboxCSS),
|
||||||
|
filepath.Join(destDir, Theme.LightboxCSS))
|
||||||
|
util.CopyFile(filepath.Join(themePath, Theme.AlbumCSS),
|
||||||
|
filepath.Join(destDir, Theme.AlbumCSS))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepImages copies images in Set set to destDir, and creates a directory of
|
||||||
|
// thumbnails at destDir.
|
||||||
|
func PrepImages(destDir string, set Set) {
|
||||||
|
dsPath := filepath.FromSlash(destDir)
|
||||||
|
util.MakeDir(filepath.Join(dsPath, set.ImageDir))
|
||||||
|
image.CopyImages(set.ImageDir, filepath.Join(dsPath, set.ImageDir),
|
||||||
|
false)
|
||||||
|
|
||||||
|
util.MakeDir(filepath.Join(dsPath, set.ImageDir, set.ThumbDir))
|
||||||
|
image.ResizeImages(filepath.Join(dsPath, set.ImageDir),
|
||||||
|
filepath.Join(dsPath, set.ImageDir, set.ThumbDir),
|
||||||
|
set.ThumbWidth, set.ThumbHeight, set.ThumbPct, set.ThumbClip,
|
||||||
|
set.ThumbUnifyMode, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPartial returns an HTML string of partial themeFile from theme themePath
|
||||||
|
// with template variables partialVars inserted.
|
||||||
|
func GetPartial(themePath, themeFile string,
|
||||||
|
partialVars map[string]string) string {
|
||||||
|
// Replace variables in partial template and trim end of file newlines.
|
||||||
|
partial := util.LoadFile(filepath.Join(filepath.FromSlash(themePath),
|
||||||
|
themeFile))
|
||||||
|
return strings.TrimRight(util.MultiReplace(partial, partialVars),
|
||||||
|
"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakePage saves a page at destPath with the contents of themeFile from
|
||||||
|
// themePath and the values from pageVars template variables inserted.
|
||||||
|
func MakePage(themePath, themeFile, destPath string,
|
||||||
|
pageVars map[string]string) {
|
||||||
|
// Replace variables in index template.
|
||||||
|
dsPath := filepath.FromSlash(destPath)
|
||||||
|
util.MakeDir(filepath.Dir(dsPath))
|
||||||
|
index := util.LoadFile(filepath.Join(filepath.FromSlash(themePath),
|
||||||
|
themeFile))
|
||||||
|
page := util.MultiReplace(index, pageVars)
|
||||||
|
os.Remove(dsPath)
|
||||||
|
util.SaveFile(dsPath, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeHTMLImagePages saves individual HTML pages for set images at destDir.
|
||||||
|
func MakeHTMLImagePages(themePath, destDir string, sets []Set) {
|
||||||
|
for _, set := range sets {
|
||||||
|
util.MakeDir(filepath.Join(destDir, set.ImageDir))
|
||||||
|
caps := GetCaptions(set.ImageDir, true)
|
||||||
|
files := image.GetImageList(set.ImageDir, set.SortOrder, false)
|
||||||
|
|
||||||
|
PrepImages(destDir, set)
|
||||||
|
for f := 0; f < len(files); f++ {
|
||||||
|
nav := ""
|
||||||
|
// No nav HTML emitted in lightbox mode.
|
||||||
|
if !set.LightboxOn {
|
||||||
|
prev, next := "#", "#"
|
||||||
|
if len(files) > 1 {
|
||||||
|
if f == 0 {
|
||||||
|
next = util.ReplaceExt(filepath.Base(files[f+1]),
|
||||||
|
filepath.Ext(Theme.Index))
|
||||||
|
} else if f == (len(files) - 1) {
|
||||||
|
prev = util.ReplaceExt(filepath.Base(files[f-1]),
|
||||||
|
filepath.Ext(Theme.Index))
|
||||||
|
next = "#"
|
||||||
|
} else {
|
||||||
|
prev = util.ReplaceExt(filepath.Base(files[f-1]),
|
||||||
|
filepath.Ext(Theme.Index))
|
||||||
|
next = util.ReplaceExt(filepath.Base(files[f+1]),
|
||||||
|
filepath.Ext(Theme.Index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navVars := map[string]string{
|
||||||
|
PartVar["navNext"]: next,
|
||||||
|
PartVar["navPrev"]: prev,
|
||||||
|
PartVar["navTop"]: set.IndexHTML,
|
||||||
|
}
|
||||||
|
nav = util.MultiReplace(PartVar["nav"], navVars)
|
||||||
|
}
|
||||||
|
partialVars := map[string]string{
|
||||||
|
ThemeVar[Theme.Image]["caption"]: caps[files[f]],
|
||||||
|
ThemeVar[Theme.Image]["path"]: filepath.Base(files[f]),
|
||||||
|
ThemeVar[Theme.Image]["title"]: image.GetImageTitle(files[f]),
|
||||||
|
ThemeVar[Theme.Image]["nav"]: nav,
|
||||||
|
}
|
||||||
|
partial := GetPartial(themePath, Theme.Image, partialVars)
|
||||||
|
pageVars := map[string]string{
|
||||||
|
ThemeVar[Theme.Index]["content"]: partial,
|
||||||
|
ThemeVar[Theme.Index]["relPath"]: "..",
|
||||||
|
ThemeVar[Theme.Index]["title"]: image.GetImageTitle(files[f]),
|
||||||
|
}
|
||||||
|
pagePath := filepath.Join(destDir, set.ImageDir,
|
||||||
|
strings.Replace(filepath.Base(files[f]),
|
||||||
|
filepath.Ext(files[f]), filepath.Ext(Theme.Index), 1))
|
||||||
|
MakePage(themePath, Theme.Index, pagePath, pageVars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeHTMLSetPages saves a page displaying each set, if enabled in the set's
|
||||||
|
// properties, at destDir.
|
||||||
|
func MakeHTMLSetPages(themePath, destDir string, sets []Set) {
|
||||||
|
tmPath := filepath.FromSlash(themePath)
|
||||||
|
dsPath := filepath.FromSlash(destDir)
|
||||||
|
setHTML := util.LoadFile(filepath.Join(tmPath, Theme.Set))
|
||||||
|
for _, set := range sets {
|
||||||
|
if set.IndexHTML != "" {
|
||||||
|
// Move images if not already present.
|
||||||
|
PrepImages(destDir, set)
|
||||||
|
|
||||||
|
// Replace variables in partial template.
|
||||||
|
setCont := GetSetHTML(set.ImageDir, set.ThumbDir, set.SortOrder,
|
||||||
|
set.LinkType, set.CSSClassLink, set.CSSClassThumb,
|
||||||
|
set.LightboxOn, false)
|
||||||
|
setDesc := GetCaption(filepath.Join(set.ImageDir, set.ImageDir))
|
||||||
|
setLB := ""
|
||||||
|
if set.LightboxOn {
|
||||||
|
setLB = GetSetLBHTML(set.ImageDir, set.SortOrder,
|
||||||
|
set.CSSClassIframe, false)
|
||||||
|
}
|
||||||
|
partialVars := map[string]string{
|
||||||
|
ThemeVar[Theme.Set]["content"]: setCont,
|
||||||
|
ThemeVar[Theme.Set]["desc"]: setDesc,
|
||||||
|
ThemeVar[Theme.Set]["lightbox"]: setLB,
|
||||||
|
ThemeVar[Theme.Set]["name"]: set.Name,
|
||||||
|
ThemeVar[Theme.Set]["title"]: set.Title,
|
||||||
|
}
|
||||||
|
pagePath := GetHTMLFilename(filepath.Join(dsPath, set.ImageDir,
|
||||||
|
set.IndexHTML))
|
||||||
|
pagePath = CheckDup(pagePath, set.ImageDir,
|
||||||
|
filepath.Ext(Theme.Index))
|
||||||
|
|
||||||
|
// Remove cached page.
|
||||||
|
os.Remove(pagePath)
|
||||||
|
setPage := strings.TrimRight(util.MultiReplace(setHTML,
|
||||||
|
partialVars), "\n")
|
||||||
|
// Replace variables in page template.
|
||||||
|
pageVars := map[string]string{
|
||||||
|
ThemeVar[Theme.Index]["content"]: setPage,
|
||||||
|
ThemeVar[Theme.Index]["relPath"]: "..",
|
||||||
|
ThemeVar[Theme.Index]["title"]: image.GetImageTitle(set.Title),
|
||||||
|
}
|
||||||
|
MakePage(themePath, Theme.Index, pagePath, pageVars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeHTMLIndexPage saves a top-level HTML page at destDir.
|
||||||
|
func MakeHTMLIndexPage(themePath, destDir string, index map[string]string,
|
||||||
|
sets []Set) {
|
||||||
|
tmPath := filepath.FromSlash(themePath)
|
||||||
|
pagePath := GetHTMLFilename(filepath.Join(filepath.FromSlash(destDir),
|
||||||
|
index["html"]))
|
||||||
|
var cache []string
|
||||||
|
|
||||||
|
if index["type"] == "sets" {
|
||||||
|
for _, set := range sets {
|
||||||
|
setHTML := util.LoadFile(filepath.Join(tmPath, Theme.Sets))
|
||||||
|
setCont := GetSetHTML(set.ImageDir, filepath.Join(set.ImageDir,
|
||||||
|
set.ThumbDir), set.SortOrder, set.LinkType,
|
||||||
|
set.CSSClassLink, set.CSSClassThumb, set.LightboxOn, true)
|
||||||
|
setDesc := GetCaption(filepath.Join(set.ImageDir, set.ImageDir))
|
||||||
|
setLB := ""
|
||||||
|
if set.LightboxOn {
|
||||||
|
setLB = GetSetLBHTML(set.ImageDir, set.SortOrder,
|
||||||
|
set.CSSClassIframe, true)
|
||||||
|
}
|
||||||
|
contVar := strings.Replace(ThemeVar[Theme.Sets]["content"],
|
||||||
|
PartVar["set"], set.Name, 1)
|
||||||
|
descVar := strings.Replace(ThemeVar[Theme.Sets]["desc"],
|
||||||
|
PartVar["set"], set.Name, 1)
|
||||||
|
lbVar := strings.Replace(ThemeVar[Theme.Sets]["lightbox"],
|
||||||
|
PartVar["set"], set.Name, 1)
|
||||||
|
nameVar := strings.Replace(ThemeVar[Theme.Sets]["name"],
|
||||||
|
PartVar["set"], set.Name, 1)
|
||||||
|
titleVar := strings.Replace(ThemeVar[Theme.Sets]["title"],
|
||||||
|
PartVar["set"], set.Name, 1)
|
||||||
|
partialVars := map[string]string{
|
||||||
|
contVar: setCont,
|
||||||
|
descVar: setDesc,
|
||||||
|
lbVar: setLB,
|
||||||
|
nameVar: set.Name,
|
||||||
|
titleVar: set.Title,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove cached page in the first pass.
|
||||||
|
// For subsequent sets on the same page, save file directly after
|
||||||
|
// replacing variables in the file already in the output directory.
|
||||||
|
if !slices.Contains(cache, pagePath) {
|
||||||
|
os.Remove(pagePath)
|
||||||
|
cache = append(cache, pagePath)
|
||||||
|
setHTML = strings.TrimRight(util.MultiReplace(setHTML,
|
||||||
|
partialVars), "\n")
|
||||||
|
pageVars := map[string]string{
|
||||||
|
ThemeVar[Theme.Index]["content"]: setHTML,
|
||||||
|
ThemeVar[Theme.Index]["relPath"]: ".",
|
||||||
|
ThemeVar[Theme.Index]["title"]: index["title"],
|
||||||
|
}
|
||||||
|
MakePage(themePath, Theme.Index, pagePath, pageVars)
|
||||||
|
} else {
|
||||||
|
setHTML = util.LoadFile(pagePath)
|
||||||
|
setHTML = strings.TrimRight(util.MultiReplace(setHTML, partialVars),
|
||||||
|
"\n")
|
||||||
|
util.SaveFile(pagePath, setHTML)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Text type.
|
||||||
|
textHTML := util.LoadFile(filepath.Join(tmPath, Theme.Text))
|
||||||
|
content := GetCaption(GetHTMLFilename(index["html"]))
|
||||||
|
partialVars := map[string]string{
|
||||||
|
ThemeVar[Theme.Text]["content"]: content,
|
||||||
|
ThemeVar[Theme.Text]["title"]: index["title"],
|
||||||
|
}
|
||||||
|
partial := strings.TrimRight(util.MultiReplace(textHTML,
|
||||||
|
partialVars), "\n")
|
||||||
|
pageVars := map[string]string{
|
||||||
|
ThemeVar[Theme.Index]["content"]: partial,
|
||||||
|
ThemeVar[Theme.Index]["relPath"]: ".",
|
||||||
|
ThemeVar[Theme.Index]["title"]: index["title"],
|
||||||
|
}
|
||||||
|
MakePage(themePath, Theme.Index, pagePath, pageVars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeAlbum generates a directory of album files.
|
||||||
|
func MakeAlbum(themePath, destDir string, index map[string]string, sets []Set) {
|
||||||
|
CopyCSS(themePath, destDir)
|
||||||
|
MakeHTMLImagePages(themePath, destDir, sets)
|
||||||
|
MakeHTMLSetPages(themePath, destDir, sets)
|
||||||
|
if index["html"] != "" {
|
||||||
|
MakeHTMLIndexPage(themePath, destDir, index, sets)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<nav class="image-nav">
|
||||||
|
{{ nav }}
|
||||||
|
</nav>
|
||||||
|
<figure class="image-fig">
|
||||||
|
<img class="img" src="{{ image_path }}" title="{{ image_title }}">
|
||||||
|
<figcaption class="caption">
|
||||||
|
{{ image_caption }}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<section class="set {{ set_name }}">
|
||||||
|
<header class="title">{{ set_title }}</header>
|
||||||
|
<div class="desc">
|
||||||
|
{{ set_desc }}
|
||||||
|
</div>
|
||||||
|
<div class="thumbs">
|
||||||
|
{{ set }}
|
||||||
|
</div>
|
||||||
|
<div class="lightbox">
|
||||||
|
{{ set_lightbox }}
|
||||||
|
</div>
|
||||||
|
</section>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<section class="set {{ set_set1_name }}">
|
||||||
|
<header class="title">{{ set_set1_title }}</header>
|
||||||
|
<div class="desc">
|
||||||
|
{{ set_set1_desc }}
|
||||||
|
</div>
|
||||||
|
<div class="thumbs">
|
||||||
|
{{ set_set1 }}
|
||||||
|
</div>
|
||||||
|
<div class="lightbox">
|
||||||
|
{{ set_set1_lightbox }}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="set {{ set_set2_name }}">
|
||||||
|
<header class="title">{{ set_set2_title }}</header>
|
||||||
|
<div class="desc">
|
||||||
|
{{ set_set2_desc }}
|
||||||
|
</div>
|
||||||
|
<div class="thumbs">
|
||||||
|
{{ set_set2 }}
|
||||||
|
</div>
|
||||||
|
<div class="lightbox">
|
||||||
|
{{ set_set2_lightbox }}
|
||||||
|
</div>
|
||||||
|
</section>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<header class="title">{{ text_title }}</header>
|
||||||
|
<section class="content">
|
||||||
|
{{ text_content }}
|
||||||
|
</section>
|
|
@ -0,0 +1,46 @@
|
||||||
|
:root {
|
||||||
|
--thumb-width: 200px;
|
||||||
|
--thumbs-row: 4;
|
||||||
|
|
||||||
|
--desc-width: 50rem;
|
||||||
|
--font: Open Sans, Helvetica Neue, Helvetica, sans-serif;
|
||||||
|
--col-body-text: rgba(10, 50, 10, 1.0);
|
||||||
|
--col-set-title: rgba(15, 110, 10, 1.0);
|
||||||
|
--col-set-desc: rgba(10, 50, 10, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: var(--col-body-text);
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set {
|
||||||
|
margin: 3rem auto 6rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set .title {
|
||||||
|
color: var(--col-set-title);
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 1rem auto;
|
||||||
|
max-width: var(--desc-width);
|
||||||
|
}
|
||||||
|
.set .desc {
|
||||||
|
color: var(--col-set-desc);
|
||||||
|
margin: 1rem auto 3rem;
|
||||||
|
max-width: var(--desc-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbs {
|
||||||
|
/* Set font size to hide newline spaces between thumbnails. */
|
||||||
|
font-size: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: calc(var(--thumb-width) * var(--thumbs-row));
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-fig .caption {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 1rem auto;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ index_title }}</title>
|
||||||
|
<link rel="stylesheet" href="{{ index_relpath }}/album.css">
|
||||||
|
<link rel="stylesheet" href="{{ index_relpath }}/lightbox.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
{{ index_content }}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,86 @@
|
||||||
|
:root {
|
||||||
|
--nav-close: 3rem;
|
||||||
|
--nav-width: 10rem;
|
||||||
|
--v-padding: 4rem;
|
||||||
|
|
||||||
|
--col-caption: rgba(15, 110, 10, 1);
|
||||||
|
--col-link: rgba(15, 110, 10, 1.0);
|
||||||
|
--col-link-visited: rgba(20, 145, 10, 1.0);
|
||||||
|
--col-overlay: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox .frame {
|
||||||
|
background: var(--col-overlay);
|
||||||
|
border: 0;
|
||||||
|
display: none;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: var(--v-padding) 0;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
.lightbox .frame:target {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.lightbox .nav {
|
||||||
|
display: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
left: calc(50% - (var(--nav-width) / 2));
|
||||||
|
max-width: var(--nav-width);
|
||||||
|
position: fixed;
|
||||||
|
top: calc(var(--v-padding) / 2);
|
||||||
|
}
|
||||||
|
.lightbox .frame:target + .nav {
|
||||||
|
display: block;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
/* Disable the prev button for the first item,
|
||||||
|
and the next button for the last item */
|
||||||
|
.lightbox .nav:nth-child(2) .prev,
|
||||||
|
.lightbox .nav:last-child .next {
|
||||||
|
opacity: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox .prev,
|
||||||
|
.lightbox .close,
|
||||||
|
.lightbox .next {
|
||||||
|
color: var(--col-link);
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.lightbox .close {
|
||||||
|
left: var(--nav-close);
|
||||||
|
}
|
||||||
|
.lightbox .next {
|
||||||
|
left: calc(var(--nav-close) * 2);
|
||||||
|
}
|
||||||
|
.lightbox .prev:visited,
|
||||||
|
.lightbox .close:visited,
|
||||||
|
.lightbox .next:visited {
|
||||||
|
color: var(--col-link-visited);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* iframe page contents */
|
||||||
|
.image-nav, .image-fig {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.image-fig .img {
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: calc(90vh - (var(--v-padding) * 2));
|
||||||
|
}
|
||||||
|
.image-fig .caption {
|
||||||
|
color: var(--col-caption);
|
||||||
|
}
|
|
@ -0,0 +1,233 @@
|
||||||
|
// Package util includes various helper functions for common
|
||||||
|
// string manipulation and file handling operations.
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Error messages.
|
||||||
|
err = struct {
|
||||||
|
dirNotCreated string
|
||||||
|
dirNotOpened string
|
||||||
|
dirNotRead string
|
||||||
|
fileNotCopied string
|
||||||
|
fileNotCreated string
|
||||||
|
fileNotLoaded string
|
||||||
|
}{
|
||||||
|
dirNotCreated: "Error: directory could not be created:",
|
||||||
|
dirNotOpened: "Error: directory could not be opened:",
|
||||||
|
dirNotRead: "Error: directory not readable:",
|
||||||
|
fileNotCopied: "Error: file could not be copied:",
|
||||||
|
fileNotCreated: "Error: file could not be created.",
|
||||||
|
fileNotLoaded: "Error: file could not be loaded:",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// HasError returns true if error e is a non-empty value and prints
|
||||||
|
// string msg to standard output, or false otherwise. If exitOnErr is
|
||||||
|
// true, sends exit signal code 3 instead of a returning a boolean value.
|
||||||
|
func HasError(e error, msg string, exitOnErr ...bool) bool {
|
||||||
|
if e != nil {
|
||||||
|
fmt.Println(msg)
|
||||||
|
if len(exitOnErr) == 1 {
|
||||||
|
if exitOnErr[0] {
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExists returns true a file or directory at path exists, or
|
||||||
|
// false otherwise.
|
||||||
|
func FileExists(path string) bool {
|
||||||
|
if _, fileErr := os.Stat(path); fileErr == nil {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinMax returns the smaller and larger value of two numbers,
|
||||||
|
// in that order.
|
||||||
|
func MinMax(n1, n2 int) (int, int) {
|
||||||
|
if n1 < n2 {
|
||||||
|
return n1, n2
|
||||||
|
} else {
|
||||||
|
return n2, n1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiReplace returns a copy of string str after replacing values
|
||||||
|
// from map of key-value string pairs rep.
|
||||||
|
func MultiReplace(str string, rep map[string]string) string {
|
||||||
|
s := str
|
||||||
|
for k, v := range rep {
|
||||||
|
s = strings.ReplaceAll(s, k, v)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MutliOrdReplace returns a copy of string str after replacing values
|
||||||
|
// from rep, a slice of maps containing key-value string pairs. Like
|
||||||
|
// MultiReplace for multiple maps.
|
||||||
|
func MultiOrdReplace(str string, rep []map[string]string) string {
|
||||||
|
s := str
|
||||||
|
for _, e := range rep {
|
||||||
|
for k, v := range e {
|
||||||
|
s = strings.ReplaceAll(s, k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceExt returns a copy of the string path with its file extension
|
||||||
|
// replaced with string ext. If path has no extension, returns a copy of path
|
||||||
|
// with ext appended to it.
|
||||||
|
func ReplaceExt(path, ext string) string {
|
||||||
|
if filepath.Ext(path) != "" {
|
||||||
|
return strings.Replace(path, filepath.Ext(path), ext, 1)
|
||||||
|
}
|
||||||
|
return path + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertPath returns a copy of string path with Unix shorthand locations
|
||||||
|
// "~/" and "$HOME" expanded to the full absolute path.
|
||||||
|
func ConvertPath(path string) string {
|
||||||
|
home, homeErr := os.UserHomeDir()
|
||||||
|
if homeErr == nil {
|
||||||
|
if strings.HasPrefix(path, "~/") {
|
||||||
|
return strings.Replace(path, "~/", home+string(os.PathSeparator), 1)
|
||||||
|
} else if strings.HasPrefix(path, "~"+filepath.Base(home)) {
|
||||||
|
return strings.Replace(path, "~"+filepath.Base(home), home, 1)
|
||||||
|
} else if strings.HasPrefix(path, "$HOME") {
|
||||||
|
return strings.Replace(path, "$HOME", home, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileList returns a string slice containing a list of files at path.
|
||||||
|
//
|
||||||
|
// sortOrder options: modTime, modTimeDesc, name, nameDesc
|
||||||
|
//
|
||||||
|
// If includeDirs is true, include directory names in the list.
|
||||||
|
func GetFileList(path string, sortOrder string, includeDirs bool) []string {
|
||||||
|
dir, openErr := os.Open(filepath.FromSlash(path))
|
||||||
|
HasError(openErr, err.dirNotOpened+" "+path)
|
||||||
|
files, readErr := dir.ReadDir(0)
|
||||||
|
HasError(readErr, err.dirNotRead+" "+path)
|
||||||
|
|
||||||
|
var list []string
|
||||||
|
if strings.Contains(sortOrder, "modTime") {
|
||||||
|
mapTimes := make(map[string]string)
|
||||||
|
var times []string
|
||||||
|
for _, file := range files {
|
||||||
|
if !file.IsDir() || (file.IsDir() && includeDirs) {
|
||||||
|
fi, _ := file.Info()
|
||||||
|
mapTimes[fmt.Sprint(fi.ModTime())] = filepath.Join(path,
|
||||||
|
fi.Name())
|
||||||
|
times = append(times, fmt.Sprint(fi.ModTime()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(times)
|
||||||
|
for _, t := range times {
|
||||||
|
list = append(list, mapTimes[t])
|
||||||
|
}
|
||||||
|
} else if strings.Contains(sortOrder, "name") {
|
||||||
|
for _, file := range files {
|
||||||
|
if !file.IsDir() || (file.IsDir() && includeDirs) {
|
||||||
|
list = append(list, filepath.Join(path, file.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(sortOrder, "Desc") {
|
||||||
|
slices.Reverse(list)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeDir returns true if a new directory is created successfully or already
|
||||||
|
// exists at path, or false otherwise. If exitOnErr is true, sends an
|
||||||
|
// application exit signal if an error occurs.
|
||||||
|
func MakeDir(path string, exitOnErr ...bool) bool {
|
||||||
|
toExit := false
|
||||||
|
if len(exitOnErr) == 1 {
|
||||||
|
if exitOnErr[0] == true {
|
||||||
|
toExit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !HasError(os.MkdirAll(filepath.FromSlash(path), 0755), err.dirNotCreated+
|
||||||
|
" "+path, toExit) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyFile returns true if a file is copied successfully from srcPath to
|
||||||
|
// destPath, or false otherwise.
|
||||||
|
func CopyFile(srcPath, destPath string) bool {
|
||||||
|
src, srcErr := os.Open(srcPath)
|
||||||
|
HasError(srcErr, err.fileNotLoaded+" "+srcPath)
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
dest, destErr := os.Create(destPath)
|
||||||
|
HasError(destErr, err.fileNotCreated)
|
||||||
|
defer dest.Close()
|
||||||
|
|
||||||
|
_, copyErr := io.Copy(dest, src)
|
||||||
|
return HasError(copyErr, err.fileNotCopied+" "+destPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyFiles copies files in srcPath to destPath. If overwrite is true,
|
||||||
|
// copying overwrites files of the same name at destPath.
|
||||||
|
func CopyFiles(srcPath string, destPath string, overwrite bool) {
|
||||||
|
files := GetFileList(srcPath, "name", false)
|
||||||
|
for _, file := range files {
|
||||||
|
destFile := filepath.Join(destPath, filepath.Base(file))
|
||||||
|
if !FileExists(destFile) || (FileExists(destFile) && overwrite) {
|
||||||
|
CopyFile(file, destFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveFile writes the string contents of str to file at path. If exitOnErr is
|
||||||
|
// true, sends an application exit signal if an error occurs.
|
||||||
|
func SaveFile(path string, str string, exitOnErr ...bool) {
|
||||||
|
fh, fileErr := os.Create(filepath.FromSlash(path))
|
||||||
|
toExit := false
|
||||||
|
if len(exitOnErr) == 1 {
|
||||||
|
if exitOnErr[0] == true {
|
||||||
|
toExit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !HasError(fileErr, err.fileNotCreated, toExit) {
|
||||||
|
fmt.Fprint(fh, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile returns the contents of path as string. If exitOnErr is true, sends
|
||||||
|
// an application exit signal if an error occurs.
|
||||||
|
func LoadFile(path string, exitOnErr ...bool) string {
|
||||||
|
contents, fileErr := os.ReadFile(filepath.FromSlash(path))
|
||||||
|
toExit := false
|
||||||
|
if len(exitOnErr) == 1 {
|
||||||
|
if exitOnErr[0] == true {
|
||||||
|
toExit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HasError(fileErr, err.fileNotLoaded+" "+path, toExit)
|
||||||
|
return string(contents)
|
||||||
|
}
|
Loading…
Reference in New Issue