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) }