snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
9
server/system/renderer/funcs.go
Normal file
9
server/system/renderer/funcs.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package renderer
|
||||
|
||||
import "github.com/cortezaproject/corteza/server/pkg/valuestore"
|
||||
|
||||
func envGetter() func(k string) any {
|
||||
return func(k string) any {
|
||||
return valuestore.Global().Env(k)
|
||||
}
|
||||
}
|
||||
108
server/system/renderer/generic_html.go
Normal file
108
server/system/renderer/generic_html.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
genericHTML struct {
|
||||
def DriverDefinition
|
||||
}
|
||||
genericHTMLDriver struct{}
|
||||
)
|
||||
|
||||
func newGenericHTML() driverFactory {
|
||||
return &genericHTML{
|
||||
def: DriverDefinition{
|
||||
Name: "genericHTML",
|
||||
InputTypes: []types.DocumentType{
|
||||
types.DocumentTypePlain,
|
||||
types.DocumentTypeHTML,
|
||||
},
|
||||
OutputTypes: []types.DocumentType{
|
||||
types.DocumentTypeHTML,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *genericHTML) Define() DriverDefinition {
|
||||
return d.def
|
||||
}
|
||||
|
||||
func (d *genericHTML) CanRender(t types.DocumentType) bool {
|
||||
for _, i := range d.def.InputTypes {
|
||||
if i == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *genericHTML) CanProduce(t types.DocumentType) bool {
|
||||
for _, o := range d.def.OutputTypes {
|
||||
if o == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *genericHTML) Driver() driver {
|
||||
return &genericHTMLDriver{}
|
||||
}
|
||||
|
||||
func (d *genericHTMLDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
|
||||
if pl.Header == nil && pl.Footer == nil {
|
||||
t, err := preprocHTMLTemplate(pl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dd := &bytes.Buffer{}
|
||||
err = t.Execute(dd, pl.Variables)
|
||||
|
||||
return bytes.NewReader(dd.Bytes()), err
|
||||
}
|
||||
|
||||
// Header and footer are rendered with the same variables and joined with
|
||||
// the content into a single document; partials are one-shot readers, so
|
||||
// they are buffered to let each of the three templates parse its own copy
|
||||
partials := make(map[string][]byte, len(pl.Partials))
|
||||
for h, r := range pl.Partials {
|
||||
bb, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
partials[h] = bb
|
||||
}
|
||||
|
||||
dd := &bytes.Buffer{}
|
||||
for _, src := range []io.Reader{pl.Header, pl.Template, pl.Footer} {
|
||||
if src == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
aux := *pl
|
||||
aux.Template = src
|
||||
aux.Partials = make(map[string]io.Reader, len(partials))
|
||||
for h, bb := range partials {
|
||||
aux.Partials[h] = bytes.NewReader(bb)
|
||||
}
|
||||
|
||||
t, err := preprocHTMLTemplate(&aux)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = t.Execute(dd, aux.Variables); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return bytes.NewReader(dd.Bytes()), nil
|
||||
}
|
||||
103
server/system/renderer/generic_html_test.go
Normal file
103
server/system/renderer/generic_html_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenericHTMLHeaderFooter(t *testing.T) {
|
||||
d := &genericHTMLDriver{}
|
||||
|
||||
pl := &driverPayload{
|
||||
Template: bytes.NewBufferString(`<main>{{template "pp" .}}</main>`),
|
||||
Header: bytes.NewBufferString(`<header>{{.name}} {{template "pp" .}}</header>`),
|
||||
Footer: bytes.NewBufferString(`<footer>{{.name}}</footer>`),
|
||||
Variables: map[string]interface{}{"name": "Human"},
|
||||
Partials: map[string]io.Reader{
|
||||
"pp": bytes.NewBufferString(`{{define "pp"}}partial:{{.name}}{{end}}`),
|
||||
},
|
||||
}
|
||||
|
||||
out, err := d.Render(context.Background(), pl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bb, _ := io.ReadAll(out)
|
||||
got := string(bb)
|
||||
want := `<header>Human partial:Human</header><main>partial:Human</main><footer>Human</footer>`
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// no header/footer keeps the single-pass path working
|
||||
pl2 := &driverPayload{
|
||||
Template: bytes.NewBufferString(`<main>ok</main>`),
|
||||
Variables: map[string]interface{}{},
|
||||
}
|
||||
out2, err := d.Render(context.Background(), pl2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bb2, _ := io.ReadAll(out2)
|
||||
if !strings.Contains(string(bb2), "<main>ok</main>") {
|
||||
t.Fatalf("plain render broken: %q", string(bb2))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericHTMLSafeHTML(t *testing.T) {
|
||||
d := &genericHTMLDriver{}
|
||||
rte := `<h1>Title 1</h1>`
|
||||
|
||||
pl := &driverPayload{
|
||||
Template: bytes.NewBufferString(`<div>{{.body}}</div><div>{{.body | safeHTML}}</div>`),
|
||||
Variables: map[string]interface{}{"body": rte},
|
||||
}
|
||||
|
||||
out, err := d.Render(context.Background(), pl)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bb, _ := io.ReadAll(out)
|
||||
got := string(bb)
|
||||
want := `<div><h1>Title 1</h1></div><div><h1>Title 1</h1></div>`
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// the case the partial workaround could not cover: rich text in a list
|
||||
pl2 := &driverPayload{
|
||||
Template: bytes.NewBufferString(`{{range .items}}<td>{{.desc | safeHTML}}</td>{{end}}`),
|
||||
Variables: map[string]interface{}{"items": []map[string]any{{"desc": rte}, {"desc": rte}}},
|
||||
}
|
||||
|
||||
out2, err := d.Render(context.Background(), pl2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bb2, _ := io.ReadAll(out2)
|
||||
if got, want := string(bb2), `<td><h1>Title 1</h1></td><td><h1>Title 1</h1></td>`; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// an unset or empty value must not break the render of the whole document
|
||||
pl3 := &driverPayload{
|
||||
Template: bytes.NewBufferString(`<td>{{.unset | safeHTML}}</td><td>{{.blank | safeHTML}}</td><td>{{.count | safeHTML}}</td>`),
|
||||
Variables: map[string]interface{}{"blank": nil, "count": 3},
|
||||
}
|
||||
|
||||
out3, err := d.Render(context.Background(), pl3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bb3, _ := io.ReadAll(out3)
|
||||
if got, want := string(bb3), `<td></td><td></td><td>3</td>`; got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
74
server/system/renderer/generic_text.go
Normal file
74
server/system/renderer/generic_text.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"regexp"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
genericText struct {
|
||||
def DriverDefinition
|
||||
}
|
||||
genericTextDriver struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
plainTextRegex = regexp.MustCompile("text(/)?.*")
|
||||
)
|
||||
|
||||
func newGenericText() driverFactory {
|
||||
return &genericText{
|
||||
def: DriverDefinition{
|
||||
Name: "genericText",
|
||||
InputTypes: []types.DocumentType{
|
||||
types.DocumentTypePlain,
|
||||
types.DocumentTypeHTML,
|
||||
},
|
||||
OutputTypes: []types.DocumentType{
|
||||
types.DocumentTypePlain,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *genericText) Define() DriverDefinition {
|
||||
return d.def
|
||||
}
|
||||
|
||||
func (d *genericText) CanRender(t types.DocumentType) bool {
|
||||
for _, i := range d.def.InputTypes {
|
||||
if i == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *genericText) CanProduce(t types.DocumentType) bool {
|
||||
for _, o := range d.def.OutputTypes {
|
||||
if o == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *genericText) Driver() driver {
|
||||
return &genericTextDriver{}
|
||||
}
|
||||
|
||||
func (d *genericTextDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
|
||||
t, err := preprocPlainTemplate(pl.Template, pl.Partials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dd := &bytes.Buffer{}
|
||||
err = t.Execute(dd, pl.Variables)
|
||||
|
||||
return bytes.NewReader(dd.Bytes()), err
|
||||
}
|
||||
387
server/system/renderer/gotenbergPDF.go
Normal file
387
server/system/renderer/gotenbergPDF.go
Normal file
@@ -0,0 +1,387 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
gotenbergPDF struct {
|
||||
url string
|
||||
def DriverDefinition
|
||||
}
|
||||
gotenbergPDFDriver struct {
|
||||
url string
|
||||
}
|
||||
)
|
||||
|
||||
// Gotenberg 7+ serves HTML conversion on the chromium module route, older (v6)
|
||||
// deployments only expose /convert/html; the working route is detected on
|
||||
// first render and cached per base URL
|
||||
var gotenbergEndpoints = sync.Map{}
|
||||
|
||||
const (
|
||||
gotenbergModernEndpoint = "/forms/chromium/convert/html"
|
||||
gotenbergLegacyEndpoint = "/convert/html"
|
||||
)
|
||||
|
||||
// @todo healthcheck, different input data formats
|
||||
func newGotenbergPDF(url string) driverFactory {
|
||||
return &gotenbergPDF{
|
||||
url: url,
|
||||
|
||||
def: DriverDefinition{
|
||||
Name: "gotenbergPDF",
|
||||
InputTypes: []types.DocumentType{
|
||||
types.DocumentTypePlain,
|
||||
types.DocumentTypeHTML,
|
||||
},
|
||||
OutputTypes: []types.DocumentType{
|
||||
types.DocumentTypePDF,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *gotenbergPDF) Define() DriverDefinition {
|
||||
return d.def
|
||||
}
|
||||
|
||||
func (d *gotenbergPDF) CanRender(t types.DocumentType) bool {
|
||||
for _, i := range d.def.InputTypes {
|
||||
if i == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *gotenbergPDF) CanProduce(t types.DocumentType) bool {
|
||||
for _, o := range d.def.OutputTypes {
|
||||
if o == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *gotenbergPDF) Driver() driver {
|
||||
return &gotenbergPDFDriver{
|
||||
url: d.url,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *gotenbergPDFDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
|
||||
// HTTP request body stuff
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
// index.html is required by the rendering container; Gotenberg 7+ requires
|
||||
// the "files" form field name, v6 accepts any name
|
||||
part, err := writer.CreateFormFile("files", "index.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = d.prepareContent(part, pl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Optional header/footer documents rendered with the same variables/partials
|
||||
if pl.Header != nil {
|
||||
hPart, err := writer.CreateFormFile("files", "header.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = d.prepareAux(hPart, pl, pl.Header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if pl.Footer != nil {
|
||||
fPart, err := writer.CreateFormFile("files", "footer.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = d.prepareAux(fPart, pl, pl.Footer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Document configurations
|
||||
err = d.applyOptions(writer, pl.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// HTTP request header stuff
|
||||
// @todo make sure to use the propper endpoint when you add support
|
||||
// for different inputs.
|
||||
url := d.url
|
||||
if n, has := pl.Options["url"]; has {
|
||||
url = n
|
||||
}
|
||||
|
||||
endpoints := []string{gotenbergModernEndpoint, gotenbergLegacyEndpoint}
|
||||
if e, has := gotenbergEndpoints.Load(url); has && e == gotenbergLegacyEndpoint {
|
||||
endpoints = []string{gotenbergLegacyEndpoint, gotenbergModernEndpoint}
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
for _, e := range endpoints {
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url+e, bytes.NewReader(body.Bytes()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 404 means the route doesn't exist on this Gotenberg version; try the other one
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
resp.Body.Close()
|
||||
resp = nil
|
||||
continue
|
||||
}
|
||||
|
||||
gotenbergEndpoints.Store(url, e)
|
||||
break
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("gotenberg render failed: no compatible endpoint found on %s", url)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
ss, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("gotenberg render failed: %s: %s", resp.Status, strings.TrimSpace(string(ss)))
|
||||
}
|
||||
|
||||
return bytes.NewReader(ss), nil
|
||||
}
|
||||
|
||||
func (d *gotenbergPDFDriver) prepareContent(w io.Writer, pl *driverPayload) error {
|
||||
t, err := preprocHTMLTemplate(pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return t.Execute(w, pl.Variables)
|
||||
}
|
||||
|
||||
// prepareAux renders an auxiliary document (header/footer) with pl's variables
|
||||
func (d *gotenbergPDFDriver) prepareAux(w io.Writer, pl *driverPayload, src io.Reader) error {
|
||||
aux := *pl
|
||||
aux.Template = src
|
||||
return d.prepareContent(w, &aux)
|
||||
}
|
||||
|
||||
func (d *gotenbergPDFDriver) applyOptions(mw *multipart.Writer, opts map[string]string) (err error) {
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for k, v := range opts {
|
||||
switch k {
|
||||
case "marginTop",
|
||||
"marginBottom",
|
||||
"marginLeft",
|
||||
"marginRight":
|
||||
err = d.addFormField(mw, k, v)
|
||||
|
||||
case "marginY":
|
||||
err = d.addFormField(mw, "marginTop", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.addFormField(mw, "marginBottom", v)
|
||||
|
||||
case "marginX":
|
||||
err = d.addFormField(mw, "marginLeft", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.addFormField(mw, "marginRight", v)
|
||||
|
||||
case "margin":
|
||||
err = d.addFormField(mw, "marginTop", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.addFormField(mw, "marginBottom", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.addFormField(mw, "marginLeft", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.addFormField(mw, "marginRight", v)
|
||||
|
||||
case "documentSize":
|
||||
w, h := d.documentDimensions(v)
|
||||
if w+h != "" {
|
||||
err = d.addFormField(mw, "paperWidth", w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.addFormField(mw, "paperHeight", h)
|
||||
}
|
||||
|
||||
case "documentWidth":
|
||||
err = d.addFormField(mw, "paperWidth", v)
|
||||
|
||||
case "documentHeight":
|
||||
err = d.addFormField(mw, "paperHeight", v)
|
||||
|
||||
case "contentScale":
|
||||
err = d.addFormField(mw, "scale", v)
|
||||
|
||||
case "orientation":
|
||||
if v == "landscape" {
|
||||
err = d.addFormField(mw, "landscape", "true")
|
||||
} else {
|
||||
err = d.addFormField(mw, "landscape", "false")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *gotenbergPDFDriver) addFormField(mw *multipart.Writer, k, v string) error {
|
||||
w, err := mw.CreateFormField(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(v))
|
||||
return err
|
||||
}
|
||||
|
||||
// documentDimensions returns the ISO216 standard document dimensions in inches (Gotenberg uses inches)
|
||||
func (d *gotenbergPDFDriver) documentDimensions(isoDoc string) (string, string) {
|
||||
switch strings.ToLower(isoDoc) {
|
||||
// A series
|
||||
case "a0":
|
||||
return "33.1", "46.8"
|
||||
case "a1":
|
||||
return "23.4", "33.1"
|
||||
case "a2":
|
||||
return "16.5", "23.4"
|
||||
case "a3":
|
||||
return "11.7", "16.5"
|
||||
case "a4":
|
||||
return "8.3", "11.7"
|
||||
case "a5":
|
||||
return "5.8", "8.3"
|
||||
case "a6":
|
||||
return "4.1", "5.8"
|
||||
case "a7":
|
||||
return "2.9", "4.1"
|
||||
case "a8":
|
||||
return "2.0", "2.9"
|
||||
case "a9":
|
||||
return "1.5", "2.0"
|
||||
case "a10":
|
||||
return "1.0", "1.5"
|
||||
|
||||
// B series
|
||||
case "b0":
|
||||
return "39.4", "55.7"
|
||||
case "b1":
|
||||
return "27.8", "39.4"
|
||||
case "b2":
|
||||
return "19.7", "27.8"
|
||||
case "b3":
|
||||
return "13.9", "19.7"
|
||||
case "b4":
|
||||
return "9.8", "13.9"
|
||||
case "b5":
|
||||
return "6.9", "9.8"
|
||||
case "b6":
|
||||
return "4.9", "6.9"
|
||||
case "b7":
|
||||
return "3.5", "4.9"
|
||||
case "b8":
|
||||
return "2.4", "3.5"
|
||||
case "b9":
|
||||
return "1.7", "2.4"
|
||||
case "b10":
|
||||
return "1.2", "1.7"
|
||||
|
||||
// C series
|
||||
case "c0":
|
||||
return "36.1", "51.1"
|
||||
case "c1":
|
||||
return "25.5", "36.1"
|
||||
case "c2":
|
||||
return "18.0", "25.5"
|
||||
case "c3":
|
||||
return "12.8", "18.0"
|
||||
case "c4":
|
||||
return "9.0", "12.8"
|
||||
case "c5":
|
||||
return "6.4", "9.0"
|
||||
case "c6":
|
||||
return "4.5", "6.4"
|
||||
case "c7":
|
||||
return "3.2", "4.5"
|
||||
case "c8":
|
||||
return "2.2", "3.2"
|
||||
case "c9":
|
||||
return "1.6", "2.2"
|
||||
case "c10":
|
||||
return "1.1", "1.6"
|
||||
|
||||
// ANSI
|
||||
case "ansi a":
|
||||
return "8.5", "11"
|
||||
case "ansi b":
|
||||
return "11", "17"
|
||||
case "ansi c":
|
||||
return "17", "22"
|
||||
case "ansi d":
|
||||
return "22", "34"
|
||||
case "ansi e":
|
||||
return "34", "44"
|
||||
|
||||
// Proprietary NA
|
||||
case "junior legal":
|
||||
return "8", "5"
|
||||
case "letter":
|
||||
return "8.5", "11"
|
||||
case "legal":
|
||||
return "8.5", "14"
|
||||
case "tabloid":
|
||||
return "11", "17"
|
||||
}
|
||||
|
||||
// This will fallback to the default (A4)
|
||||
return "", ""
|
||||
}
|
||||
87
server/system/renderer/html.go
Normal file
87
server/system/renderer/html.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
)
|
||||
|
||||
func preprocHTMLTemplate(pl *driverPayload) (*template.Template, error) {
|
||||
bb, err := ioutil.ReadAll(pl.Template)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gtpl := template.New("text/html_render").
|
||||
Funcs(sprig.FuncMap()).
|
||||
Funcs(template.FuncMap{
|
||||
// "attachDataURL": func(name string) template.URL {
|
||||
// // Find the attachment
|
||||
// att, has := pl.Attachments[name]
|
||||
// if !has {
|
||||
// return template.URL(fmt.Sprintf("error: attachment not found: %s", name))
|
||||
// }
|
||||
|
||||
// // Process source
|
||||
// bb, err := ioutil.ReadAll(att.Source)
|
||||
// if err != nil {
|
||||
// return template.URL(fmt.Sprintf("error: %s", err.Error()))
|
||||
// }
|
||||
|
||||
// return template.URL("data:" + att.Mime + ";base64," + base64.RawStdEncoding.EncodeToString(bb))
|
||||
// },
|
||||
|
||||
"inlineRemote": func(url string) (template.URL, error) {
|
||||
rsp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer rsp.Body.Close()
|
||||
|
||||
bb, err := ioutil.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
raw := base64.RawStdEncoding.EncodeToString(bb)
|
||||
return template.URL("data:" + rsp.Header.Get("Content-Type") + ";base64," + raw), nil
|
||||
},
|
||||
// emits the value as-is, without the contextual escaping
|
||||
// html/template applies to every other interpolated value;
|
||||
// unset values render empty, the same as plain interpolation
|
||||
"safeHTML": func(v any) template.HTML {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return template.HTML(fmt.Sprintf("%v", v))
|
||||
},
|
||||
"env": envGetter(),
|
||||
})
|
||||
|
||||
// Prep the original template
|
||||
t, err := gtpl.Parse(string(bb))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prep partials
|
||||
for _, p := range pl.Partials {
|
||||
bb, err = ioutil.ReadAll(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err = gtpl.Parse(string(bb))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
59
server/system/renderer/renderer.go
Normal file
59
server/system/renderer/renderer.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
)
|
||||
|
||||
type (
|
||||
renderer struct {
|
||||
factories []driverFactory
|
||||
}
|
||||
)
|
||||
|
||||
func Renderer(cfg options.TemplateOpt) *renderer {
|
||||
ff := make([]driverFactory, 0, 3)
|
||||
ff = append(ff, newGenericText(), newGenericHTML())
|
||||
if cfg.RendererGotenbergEnabled {
|
||||
ff = append(ff, newGotenbergPDF(cfg.RendererGotenbergAddress))
|
||||
}
|
||||
|
||||
return &renderer{
|
||||
factories: ff,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *renderer) Render(ctx context.Context, pl *RendererPayload) (io.ReadSeeker, error) {
|
||||
for _, f := range r.factories {
|
||||
if f.CanRender(pl.TemplateType) && f.CanProduce(pl.TargetType) {
|
||||
pp := make(map[string]io.Reader)
|
||||
for _, prt := range pl.Partials {
|
||||
pp[prt.Handle] = prt.Template
|
||||
}
|
||||
dpl := &driverPayload{
|
||||
Template: pl.Template,
|
||||
Variables: pl.Variables,
|
||||
Options: pl.Options,
|
||||
Partials: pp,
|
||||
Attachments: pl.Attachments,
|
||||
Header: pl.Header,
|
||||
Footer: pl.Footer,
|
||||
}
|
||||
|
||||
return f.Driver().Render(ctx, dpl)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("rendering failed: driver not found")
|
||||
}
|
||||
|
||||
func (r *renderer) Drivers() []DriverDefinition {
|
||||
dd := make([]DriverDefinition, len(r.factories))
|
||||
for i, f := range r.factories {
|
||||
dd[i] = f.Define()
|
||||
}
|
||||
return dd
|
||||
}
|
||||
43
server/system/renderer/text.go
Normal file
43
server/system/renderer/text.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
)
|
||||
|
||||
func preprocPlainTemplate(tpl io.Reader, pp map[string]io.Reader) (*template.Template, error) {
|
||||
bb, err := ioutil.ReadAll(tpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gtpl := template.New("text/plain_render").
|
||||
Funcs(sprig.TxtFuncMap()).
|
||||
Funcs(template.FuncMap{
|
||||
"env": envGetter(),
|
||||
})
|
||||
|
||||
// Prep the original template
|
||||
t, err := gtpl.Parse(string(bb))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prep partials
|
||||
for _, p := range pp {
|
||||
bb, err = ioutil.ReadAll(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err = gtpl.Parse(string(bb))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
65
server/system/renderer/types.go
Normal file
65
server/system/renderer/types.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
)
|
||||
|
||||
type (
|
||||
RendererPayload struct {
|
||||
Template io.Reader
|
||||
TemplateType types.DocumentType
|
||||
TargetType types.DocumentType
|
||||
Variables map[string]interface{}
|
||||
Options map[string]string
|
||||
Partials []*TemplatePartial
|
||||
Attachments AttachmentIndex
|
||||
|
||||
// Optional header/footer documents, attached by drivers that support them
|
||||
Header io.Reader
|
||||
Footer io.Reader
|
||||
}
|
||||
|
||||
TemplatePartial struct {
|
||||
Handle string
|
||||
Template io.Reader
|
||||
TemplateType types.DocumentType
|
||||
}
|
||||
|
||||
driverPayload struct {
|
||||
Template io.Reader
|
||||
Variables map[string]interface{}
|
||||
Options map[string]string
|
||||
Partials map[string]io.Reader
|
||||
Attachments AttachmentIndex
|
||||
|
||||
Header io.Reader
|
||||
Footer io.Reader
|
||||
}
|
||||
|
||||
AttachmentIndex map[string]*Attachment
|
||||
Attachment struct {
|
||||
Source io.Reader
|
||||
Mime string
|
||||
Name string
|
||||
}
|
||||
|
||||
DriverDefinition struct {
|
||||
Name string `json:"name"`
|
||||
InputTypes []types.DocumentType `json:"inputTypes"`
|
||||
OutputTypes []types.DocumentType `json:"outputTypes"`
|
||||
}
|
||||
|
||||
driverFactory interface {
|
||||
Define() DriverDefinition
|
||||
|
||||
CanRender(t types.DocumentType) bool
|
||||
CanProduce(t types.DocumentType) bool
|
||||
Driver() driver
|
||||
}
|
||||
driver interface {
|
||||
Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error)
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user