go-cv/generator.go

248 lines
4.6 KiB
Go

package cv
import (
"encoding/json"
"github.com/SebastiaanKlippert/go-wkhtmltopdf"
"github.com/pkg/errors"
"github.com/spf13/afero"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/html"
"github.com/tdewolff/minify/v2/js"
"github.com/tdewolff/minify/v2/svg"
"github.com/tdewolff/minify/v2/xml"
"github.com/tystuyfzand/less-go"
"html/template"
"io"
"mime"
"os"
"path"
"path/filepath"
"regexp"
"strings"
)
type generator struct {
fs afero.Fs
output afero.Fs
tpl *template.Template
less *less.Compiler
}
// Generate creates the CV from the input data.
// TODO: Allow output to be S3/similar.
func Generate() error {
fs := afero.NewOsFs()
g := &generator{
fs: fs,
output: afero.NewBasePathFs(fs, "output"),
}
if err := g.setupTemplate(); err != nil {
return err
}
return g.Execute("data/cv.json")
}
// loadCvData will read the CV data from JSON.
func (g *generator) loadCvData(path string) (*cvData, error) {
f, err := g.fs.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
data := cvData{}
if err = json.NewDecoder(f).Decode(&data); err != nil {
return nil, err
}
return &data, nil
}
// Execute runs all steps of the render (assets, template rendering, pdf)
func (g *generator) Execute(file string) error {
data, err := g.loadCvData("data/cv.json")
if err != nil {
return err
}
if err = g.copyAssets(); err != nil {
return err
}
if err = g.render(data); err != nil {
return err
}
if err = g.renderPdf(); err != nil {
return err
}
return nil
}
// copyAssets will copy all of the assets in the "assets" folder to the output.
// It also compiles any LESS files into css, minifies files, etc.
// TODO: Add support for png and jpeg images using mozjpeg and pngquant.
func (g *generator) copyAssets() error {
if g.less == nil {
var err error
g.less, err = less.NewCompiler()
if err != nil {
return err
}
g.less.SetReader(&fsReader{g.fs})
g.less.SetWriter(&fsWriter{g.output})
}
assets, err := afero.Glob(g.fs, "assets/**")
if err != nil {
return err
}
m := minify.New()
m.AddFunc("text/css", css.Minify)
m.AddFunc("text/html", html.Minify)
m.AddFunc("image/svg+xml", svg.Minify)
m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify)
m.AddFuncRegexp(regexp.MustCompile("[/+]xml$"), xml.Minify)
var dir, outputFile string
for _, asset := range assets {
src, err := g.fs.Open(asset)
if err != nil {
return errors.Wrap(err, "unable to open asset file")
}
dir = filepath.Dir(asset)
if _, err = g.output.Stat(dir); os.IsNotExist(err) {
err = g.output.MkdirAll(dir, 0664)
if err != nil {
return errors.Wrap(err, "unable to create output sub directory")
}
}
outputFile = asset
switch path.Ext(asset) {
case ".less":
outputFile = strings.TrimSuffix(asset, ".less") + ".css"
}
dest, err := g.output.Create(outputFile)
if err != nil {
return errors.Wrap(err, "unable to create output file")
}
ext := path.Ext(asset)
switch ext {
case ".less":
var res string
res, err = g.less.RenderFile(asset, map[string]interface{}{
"compress": true,
})
if err != nil {
return errors.Wrap(err, "unable to compile resource")
}
_, err = dest.Write([]byte(res))
case ".css", ".js", ".svg":
mimeType := mime.TypeByExtension(ext)
if err := m.Minify(mimeType, dest, src); err != nil {
return err
}
default:
_, err = io.Copy(dest, src)
}
if err != nil {
return errors.Wrap(err, "unable to copy resource")
}
dest.Close()
src.Close()
}
return nil
}
// renderPdf will render a pdf version of the output html.
// TODO: This requires a local copy (likely using a cloned output) for rendering.
func (g *generator) renderPdf() error {
pdf, err := wkhtmltopdf.NewPDFGenerator()
if err != nil {
return err
}
pdf.MarginTop.Set(0)
pdf.MarginLeft.Set(0)
pdf.MarginBottom.Set(0)
pdf.MarginRight.Set(0)
out, err := g.output.Create("index.pdf")
if err != nil {
return err
}
defer out.Close()
pdf.SetOutput(out)
page := wkhtmltopdf.NewPage("output/index.html")
page.PrintMediaType.Set(true)
page.DisableSmartShrinking.Set(true)
pdf.AddPage(page)
err = pdf.Create()
if err != nil {
return err
}
return nil
}
// render is an internal method to render html via the loaded template.
func (g *generator) render(data interface{}) error {
f, err := g.output.Create("index.html")
if err != nil {
return err
}
defer f.Close()
m := minify.New()
m.AddFunc("text/html", html.Minify)
w := m.Writer("text/html", f)
defer w.Close()
return g.tpl.Execute(w, data)
}