aboutsummaryrefslogtreecommitdiffhomepage
path: root/tpl
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2024-08-07 10:40:54 +0200
committerBjørn Erik Pedersen <[email protected]>2024-08-09 17:18:37 +0200
commit33c0938cd50dd3409f8e94878b97d789cc328f23 (patch)
treefc4cc45265b86746aa37bc3ab4445724d22a98f2 /tpl
parent0c3a1c7288032401327a9c4d7044e297bf3f7da6 (diff)
downloadhugo-33c0938cd50dd3409f8e94878b97d789cc328f23.tar.gz
hugo-33c0938cd50dd3409f8e94878b97d789cc328f23.zip
Add build time math rendering
While very useful on its own (and combined with the passthrough render hooks), this also serves as a proof of concept of using WASI (WebAssembly System Interface) modules in Hugo. This will be marked _experimental_ in the documentation. Not because it will be removed or changed in a dramatic way, but we need to think a little more how to best set up/configure similar services, define where these WASM files gets stored, maybe we can allow user provided WASM files plugins via Hugo Modules mounts etc. See these issues for more context: * https://github.com/gohugoio/hugo/issues/12736 * https://github.com/gohugoio/hugo/issues/12737 See #11927
Diffstat (limited to 'tpl')
-rw-r--r--tpl/transform/transform.go82
-rw-r--r--tpl/transform/transform_integration_test.go17
-rw-r--r--tpl/transform/unmarshal.go4
3 files changed, 96 insertions, 7 deletions
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index 5ef9bff21..db7703b7f 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -18,16 +18,23 @@ import (
"bytes"
"context"
"encoding/xml"
+ "errors"
"html"
"html/template"
+ "io"
"strings"
+ "sync/atomic"
"github.com/gohugoio/hugo/cache/dynacache"
+ "github.com/gohugoio/hugo/common/hashing"
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/internal/warpc"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/markup/highlight/chromalexers"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/tpl"
+ "github.com/mitchellh/mapstructure"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers"
@@ -42,18 +49,26 @@ func New(deps *deps.Deps) *Namespace {
return &Namespace{
deps: deps,
- cache: dynacache.GetOrCreatePartition[string, *resources.StaleValue[any]](
+ cacheUnmarshal: dynacache.GetOrCreatePartition[string, *resources.StaleValue[any]](
deps.MemCache,
- "/tmpl/transform",
+ "/tmpl/transform/unmarshal",
dynacache.OptionsPartition{Weight: 30, ClearWhen: dynacache.ClearOnChange},
),
+ cacheMath: dynacache.GetOrCreatePartition[string, string](
+ deps.MemCache,
+ "/tmpl/transform/math",
+ dynacache.OptionsPartition{Weight: 30, ClearWhen: dynacache.ClearNever},
+ ),
}
}
// Namespace provides template functions for the "transform" namespace.
type Namespace struct {
- cache *dynacache.Partition[string, *resources.StaleValue[any]]
- deps *deps.Deps
+ cacheUnmarshal *dynacache.Partition[string, *resources.StaleValue[any]]
+ cacheMath *dynacache.Partition[string, string]
+
+ id atomic.Uint32
+ deps *deps.Deps
}
// Emojify returns a copy of s with all emoji codes replaced with actual emojis.
@@ -182,7 +197,64 @@ func (ns *Namespace) Plainify(s any) (string, error) {
return tpl.StripHTML(ss), nil
}
+// 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) (string, error) {
+ if len(args) < 1 {
+ return "", errors.New("must provide at least one argument")
+ }
+ expression, err := cast.ToStringE(args[0])
+ if err != nil {
+ return "", err
+ }
+
+ katexInput := warpc.KatexInput{
+ Expression: expression,
+ Options: warpc.KatexOptions{
+ Output: "mathml",
+ ThrowOnError: false,
+ },
+ }
+
+ if len(args) > 1 {
+ if err := mapstructure.WeakDecode(args[1], &katexInput); err != nil {
+ return "", err
+ }
+ }
+
+ s := hashing.HashString(args...)
+ key := "tomath/" + s[:2] + "/" + s[2:]
+ fileCache := ns.deps.ResourceSpec.FileCaches.MiscCache()
+
+ return ns.cacheMath.GetOrCreate(key, func(string) (string, error) {
+ _, r, err := fileCache.GetOrCreate(key, func() (io.ReadCloser, error) {
+ message := warpc.Message[warpc.KatexInput]{
+ Header: warpc.Header{
+ Version: "v1",
+ ID: ns.id.Add(1),
+ },
+ Data: katexInput,
+ }
+
+ k, err := ns.deps.WasmDispatchers.Katex()
+ if err != nil {
+ return nil, err
+ }
+ result, err := k.Execute(ctx, message)
+ if err != nil {
+ return nil, err
+ }
+ return hugio.NewReadSeekerNoOpCloserFromString(result.Data.Output), nil
+ })
+ if err != nil {
+ return "", err
+ }
+
+ return hugio.ReadString(r)
+ })
+}
+
// For internal use.
func (ns *Namespace) Reset() {
- ns.cache.Clear()
+ ns.cacheUnmarshal.Clear()
}
diff --git a/tpl/transform/transform_integration_test.go b/tpl/transform/transform_integration_test.go
index 351420a67..529f18a5f 100644
--- a/tpl/transform/transform_integration_test.go
+++ b/tpl/transform/transform_integration_test.go
@@ -133,3 +133,20 @@ Scar,"a "dead cat",11
[[name description age] [Spot a nice dog 3] [Rover a big dog 5] [Felix a "malicious" cat 7] [Bella an "evil" cat 9] [Scar a "dead cat 11]]
`)
}
+
+func TestToMath(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/index.html --
+{{ $result := transform.ToMath "c = \\pm\\sqrt{a^2 + b^2}" }}
+{{ printf "%v" $result | safeHTML }}
+ `
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", `
+<span class="katex"><math
+ `)
+}
diff --git a/tpl/transform/unmarshal.go b/tpl/transform/unmarshal.go
index 23b99d91f..898085661 100644
--- a/tpl/transform/unmarshal.go
+++ b/tpl/transform/unmarshal.go
@@ -71,7 +71,7 @@ func (ns *Namespace) Unmarshal(args ...any) (any, error) {
key += decoder.OptionsKey()
}
- v, err := ns.cache.GetOrCreate(key, func(string) (*resources.StaleValue[any], error) {
+ v, err := ns.cacheUnmarshal.GetOrCreate(key, func(string) (*resources.StaleValue[any], error) {
f := metadecoders.FormatFromStrings(r.MediaType().Suffixes()...)
if f == "" {
return nil, fmt.Errorf("MIME %q not supported", r.MediaType())
@@ -119,7 +119,7 @@ func (ns *Namespace) Unmarshal(args ...any) (any, error) {
key := hashing.MD5FromStringHexEncoded(dataStr)
- v, err := ns.cache.GetOrCreate(key, func(string) (*resources.StaleValue[any], error) {
+ v, err := ns.cacheUnmarshal.GetOrCreate(key, func(string) (*resources.StaleValue[any], error) {
f := decoder.FormatFromContentString(dataStr)
if f == "" {
return nil, errors.New("unknown format")