Initial commit
This commit is contained in:
248
generator.go
Normal file
248
generator.go
Normal file
@@ -0,0 +1,248 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user