Skip to content

Commit

Permalink
Improve Katex error handling
Browse files Browse the repository at this point in the history
* Make throwOnError=true the new default
* Handle JS errors as part of the RPC request/response flow
* Return a new Result type with .Err on it

This enables constructs on the form:

```handlebars
{{ with transform.ToMath "c = \\foo{a^2 + b^2}" }}
	{{ with .Err }}
	 	{{ warnf "error: %s" . }}
	{{ else }}
		{{ . }}
	{{ end }}
{{ end }}
```

Note that the new `Result` type behaves like `template.HTML` (or a string if needed) when printed, but it will panic if in a error state.

Closes gohugoio#12748
  • Loading branch information
bep committed Aug 12, 2024
1 parent e422635 commit f0237bf
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 18 deletions.
24 changes: 24 additions & 0 deletions common/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,27 @@ var InvocationCounter atomic.Int64
func NewBool(b bool) *bool {
return &b
}

// PrintableValueProvider is implemented by types that can provide a printable value.
type PrintableValueProvider interface {
PrintableValue() any
}

var _ PrintableValueProvider = Result[any]{}

// Result is a generic result type.
type Result[T any] struct {
// The result value.
Value T

// The error value.
Err error
}

// PrintableValue returns the value or panics if there is an error.
func (r Result[T]) PrintableValue() any {
if r.Err != nil {
panic(r.Err)
}
return r.Value
}
14 changes: 7 additions & 7 deletions internal/warpc/js/renderkatex.bundle.js

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion internal/warpc/js/renderkatex.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ const render = function (input) {
const data = input.data;
const expression = data.expression;
const options = data.options;
writeOutput({ header: input.header, data: { output: katex.renderToString(expression, options) } });
const header = input.header;
try {
const output = katex.renderToString(expression, options);
writeOutput({ header: header, data: { output: output } });
} catch (e) {
header.err = e.message;
writeOutput({ header: header });
}
};

readInput(render);
1 change: 0 additions & 1 deletion internal/warpc/katex.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ type KatexOptions struct {
MinRuleThickness float64 `json:"minRuleThickness"`

// If true, KaTeX will throw a ParseError when it encounters an unsupported command.
// For internal use only, for now.
ThrowOnError bool `json:"throwOnError"`
}

Expand Down
3 changes: 3 additions & 0 deletions internal/warpc/warpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ var quickjsWasm []byte
type Header struct {
Version string `json:"version"`
ID uint32 `json:"id"`

// Set in the response if there was an error.
Err string `json:"err"`
}

type Message[T any] struct {
Expand Down
Binary file modified internal/warpc/wasm/renderkatex.wasm
Binary file not shown.
3 changes: 2 additions & 1 deletion tpl/internal/go_templates/htmltemplate/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const (

// indirect returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil).
func indirect(a any) any {
// Signature modified by Hugo. TODO(bep) script this.
func doIndirect(a any) any {
if a == nil {
return nil
}
Expand Down
12 changes: 12 additions & 0 deletions tpl/internal/go_templates/htmltemplate/hugo_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package template

import (
"github.com/gohugoio/hugo/common/types"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
)

Expand All @@ -39,3 +40,14 @@ func (t *Template) Prepare() (*template.Template, error) {
func StripTags(html string) string {
return stripTags(html)
}

func indirect(a any) any {
in := doIndirect(a)

// We have a special Result type that we want to unwrap when printed.
if pp, ok := in.(types.PrintableValueProvider); ok {
return pp.PrintableValue()
}

return in
}
27 changes: 19 additions & 8 deletions tpl/transform/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/gohugoio/hugo/cache/dynacache"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/internal/warpc"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/highlight"
Expand Down Expand Up @@ -199,13 +200,15 @@ func (ns *Namespace) Plainify(s any) (template.HTML, error) {

// ToMath converts a LaTeX string to math in the given format, default MathML.
// This uses KaTeX to render the math, see https://katex.org/.
func (ns *Namespace) ToMath(ctx context.Context, args ...any) (template.HTML, error) {
func (ns *Namespace) ToMath(ctx context.Context, args ...any) (types.Result[template.HTML], error) {
var res types.Result[template.HTML]

if len(args) < 1 {
return "", errors.New("must provide at least one argument")
return res, errors.New("must provide at least one argument")
}
expression, err := cast.ToStringE(args[0])
if err != nil {
return "", err
return res, err
}

katexInput := warpc.KatexInput{
Expand All @@ -214,23 +217,21 @@ func (ns *Namespace) ToMath(ctx context.Context, args ...any) (template.HTML, er
Output: "mathml",
MinRuleThickness: 0.04,
ErrorColor: "#cc0000",
ThrowOnError: true,
},
}

if len(args) > 1 {
if err := mapstructure.WeakDecode(args[1], &katexInput.Options); err != nil {
return "", err
return res, err
}
}

// Make sure this isn't set by the client (for now).
katexInput.Options.ThrowOnError = false

s := hashing.HashString(args...)
key := "tomath/" + s[:2] + "/" + s[2:]
fileCache := ns.deps.ResourceSpec.FileCaches.MiscCache()

return ns.cacheMath.GetOrCreate(key, func(string) (template.HTML, error) {
v, err := ns.cacheMath.GetOrCreate(key, func(string) (template.HTML, error) {
_, r, err := fileCache.GetOrCreate(key, func() (io.ReadCloser, error) {
message := warpc.Message[warpc.KatexInput]{
Header: warpc.Header{
Expand All @@ -248,6 +249,9 @@ func (ns *Namespace) ToMath(ctx context.Context, args ...any) (template.HTML, er
if err != nil {
return nil, err
}
if result.Header.Err != "" {
return nil, errors.New(result.Header.Err)
}
return hugio.NewReadSeekerNoOpCloserFromString(result.Data.Output), nil
})
if err != nil {
Expand All @@ -258,6 +262,13 @@ func (ns *Namespace) ToMath(ctx context.Context, args ...any) (template.HTML, er

return template.HTML(s), err
})

res = types.Result[template.HTML]{
Value: v,
Err: err,
}

return res, nil
}

// For internal use.
Expand Down
54 changes: 54 additions & 0 deletions tpl/transform/transform_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,60 @@ disableKinds = ['page','rss','section','sitemap','taxonomy','term']
`)
}

func TestToMathError(t *testing.T) {
t.Run("Default", func(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
-- layouts/index.html --
{{ transform.ToMath "c = \\foo{a^2 + b^2}" }}
`
b, err := hugolib.TestE(t, files, hugolib.TestOptWarn())

b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "KaTeX parse error: Undefined control sequence: \\foo")
})

t.Run("Disable ThrowOnError", func(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
-- layouts/index.html --
{{ $opts := dict "throwOnError" false }}
{{ transform.ToMath "c = \\foo{a^2 + b^2}" $opts }}
`
b, err := hugolib.TestE(t, files, hugolib.TestOptWarn())

b.Assert(err, qt.IsNil)
b.AssertFileContent("public/index.html", `#cc0000`) // Error color
})

t.Run("Handle in template", func(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
-- layouts/index.html --
{{ with transform.ToMath "c = \\foo{a^2 + b^2}" }}
{{ with .Err }}
{{ warnf "error: %s" . }}
{{ else }}
{{ . }}
{{ end }}
{{ end }}
`
b, err := hugolib.TestE(t, files, hugolib.TestOptWarn())

b.Assert(err, qt.IsNil)
b.AssertLogContains("WARN error: KaTeX parse error: Undefined control sequence: \\foo")
})
}

func TestToMathMacros(t *testing.T) {
t.Parallel()

Expand Down

0 comments on commit f0237bf

Please sign in to comment.