248 lines
4.6 KiB
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)
|
|
} |