diff options
author | Bjørn Erik Pedersen <[email protected]> | 2024-08-07 10:40:54 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2024-08-09 17:18:37 +0200 |
commit | 33c0938cd50dd3409f8e94878b97d789cc328f23 (patch) | |
tree | fc4cc45265b86746aa37bc3ab4445724d22a98f2 /tpl | |
parent | 0c3a1c7288032401327a9c4d7044e297bf3f7da6 (diff) | |
download | hugo-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.go | 82 | ||||
-rw-r--r-- | tpl/transform/transform_integration_test.go | 17 | ||||
-rw-r--r-- | tpl/transform/unmarshal.go | 4 |
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") |