diff --git a/docs/content/en/functions/external.md b/docs/content/en/functions/external.md new file mode 100644 index 00000000000..706c6155167 --- /dev/null +++ b/docs/content/en/functions/external.md @@ -0,0 +1,75 @@ +--- +title: fn +description: Calls a custom, external Typescript/Jacascript function. +categories: [functions] +menu: + docs: + parent: functions +keywords: [function, external, code] +signature: ["external.Function FILE.FUNCTION ARGUMENTS…", "fn FILE.FUNCTION ARGUMENTS…"] +relatedfuncs: [] +--- + +This function allows the use of custom, portable Typescript/Javascript code stored within the hugo site files. + +For example if the file `functions/hello.ts` exists in a site with the exported function `Name`: + +```typescript +export function Name(name: string): string { + return `Hello ${name || "World"}!` +} +``` + +Then a Hugo template can call the external function `hello.Name`: + +```go-html-template + +``` + +Which will call the custom code and placing the results in the page: + +```html + +``` + +## Naming + +Capitalisation matters in the file/function name; calling `{{ fn "hello.name" }}` in the example above would fail with: + +```plain +error calling fn: the function named name does not exist in hello +``` + +Similarly calling `{{ fn "heLLo.Name" }}` would fail with: + +```plain +error calling fn: the function file named heLLo has not been loaded +``` + +## Function signatures + +The exported functions can accept any number of arguments, of any type, but must only return a single string. any exceptions `throw`n will be handled gracefully. + +Arguments are automatically converted to Javascript native formats using [goja's `ToValue` method](https://pkg.go.dev/github.com/dop251/goja#Runtime.ToValue)). + +Dates/times are also converted to native `Date` objects (ie. _not_ like [default goja](https://pkg.go.dev/github.com/dop251/goja#hdr-Handling_of_time_Time)). This does mean that Timezone information is lost; the `Date` object in your function will be in the timezone of the machine Hugo is running on (not the timezone of the passed argument). You can work around this by sending any needed timezone information as a separate argument, eg: + +```go-template +{{ fn "example.Timezone" .Date (.Date.Format "-0700") }} +``` + +## Imports + +Though the [Almond AMD loader](https://github.com/requirejs/almond) is readily available (via [clarkmcc's go-typescript](https://github.com/clarkmcc/go-typescript)) the Typescript execution environment does not have access to the filesystem. + +(Perhaps encourage to use webpack or similar to build single file?) diff --git a/go.mod b/go.mod index 18847d14271..3939a3f5f9d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ require ( github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 github.com/PuerkitoBio/purell v1.1.1 github.com/alecthomas/chroma/v2 v2.7.0 + github.com/apitoolkit/doctests v0.0.0-20220724172649-f36dc3e57bf8 github.com/armon/go-radix v1.0.0 github.com/aws/aws-sdk-go v1.43.5 github.com/bep/clock v0.3.0 @@ -18,9 +19,11 @@ require ( github.com/bep/overlayfs v0.6.0 github.com/bep/simplecobra v0.3.1 github.com/bep/tmc v0.5.1 + github.com/clarkmcc/go-typescript v0.7.0 github.com/clbanning/mxj/v2 v2.5.7 github.com/cli/safeexec v1.0.1 github.com/disintegration/gift v1.2.1 + github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 github.com/dustin/go-humanize v1.0.1 github.com/evanw/esbuild v0.17.19 github.com/fortytw2/leaktest v1.3.0 @@ -60,6 +63,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/tdewolff/minify/v2 v2.12.6 github.com/tdewolff/parse/v2 v2.6.6 + github.com/wasmerio/wasmer-go v1.0.4 github.com/yuin/goldmark v1.5.4 go.uber.org/atomic v1.10.0 go.uber.org/automaxprocs v1.5.2 @@ -102,9 +106,11 @@ require ( github.com/dlclark/regexp2 v1.10.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/swag v0.19.5 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/google/s2a-go v0.1.3 // indirect github.com/google/uuid v1.3.0 // indirect github.com/google/wire v0.5.0 // indirect @@ -119,10 +125,11 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-ieproxy v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/perimeterx/marshmallow v1.1.4 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.3.0 // indirect diff --git a/go.sum b/go.sum index f02171e68dc..017f4b98125 100644 --- a/go.sum +++ b/go.sum @@ -131,6 +131,8 @@ github.com/alecthomas/chroma/v2 v2.7.0 h1:hm1rY6c/Ob4eGclpQ7X/A3yhqBOZNUTk9q+yhy github.com/alecthomas/chroma/v2 v2.7.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apitoolkit/doctests v0.0.0-20220724172649-f36dc3e57bf8 h1:fmoAv97b/HW49lAcOK9Vwlbaqsp5IMZuXBxiFezZc7E= +github.com/apitoolkit/doctests v0.0.0-20220724172649-f36dc3e57bf8/go.mod h1:KEnxZQRHcFa1CHfKU1B40Ds+rscvWLKrXFW03iQOdms= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= @@ -192,8 +194,13 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clarkmcc/go-typescript v0.7.0 h1:3nVeaPYyTCWjX6Lf8GoEOTxME2bM5tLuWmwhSZ86uxg= +github.com/clarkmcc/go-typescript v0.7.0/go.mod h1:IZ/nzoVeydAmyfX7l6Jmp8lJDOEnae3jffoXwP4UyYg= github.com/clbanning/mxj/v2 v2.5.7 h1:7q5lvUpaPF/WOkqgIDiwjBJaznaLCCBd78pi8ZyAnE0= github.com/clbanning/mxj/v2 v2.5.7/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= @@ -225,8 +232,15 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/ github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 h1:+3HCtB74++ClLy8GgjUQYeC8R4ILzVcIe8+5edAJJnE= +github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -270,6 +284,8 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= @@ -373,6 +389,8 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE= github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= @@ -405,6 +423,7 @@ github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= @@ -457,6 +476,8 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= @@ -493,6 +514,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -548,6 +571,8 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/wasmerio/wasmer-go v1.0.4 h1:MnqHoOGfiQ8MMq2RF6wyCeebKOe84G88h5yv+vmxJgs= +github.com/wasmerio/wasmer-go v1.0.4/go.mod h1:0gzVdSfg6pysA6QVp6iVRPTagC6Wq9pOE8J86WKb2Fk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -777,6 +802,7 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/tpl/external/init.go b/tpl/external/init.go new file mode 100644 index 00000000000..de7a81819c3 --- /dev/null +++ b/tpl/external/init.go @@ -0,0 +1,33 @@ +package external + +import ( + "context" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/tpl/internal" +) + +// TODO: Pull functions location from config.toml, with this as default +const defaultFunctionsPath = "functions/" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx, err := LoadFunctionFiles(defaultFunctionsPath) + if err != nil { + helpers.DistinctErrorLog.Warnf("Unable to load any function files: %v\n", err) + return nil + } + + ns := &internal.TemplateFuncsNamespace{ + Name: "external", + Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil }, + } + + ns.AddMethodMapping(ctx.Function, []string{"fn"}, nil) + + return ns + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/external/load.go b/tpl/external/load.go new file mode 100644 index 00000000000..604696f3807 --- /dev/null +++ b/tpl/external/load.go @@ -0,0 +1,187 @@ +package external + +import ( + "context" + "fmt" + "os" + "path" + "path/filepath" + "reflect" + "strings" + "time" + + "github.com/clarkmcc/go-typescript" + "github.com/dop251/goja" + "github.com/gohugoio/hugo/helpers" +) + +const maxFuncFileLoadTime = time.Second + +type FunctionDetails struct { + Name string + Func func(args ...reflect.Value) (string, error) + Examples [][2]string +} + +type Namespace struct { + funcs map[string]map[string]func(...reflect.Value) (string, error) +} + +func (ns *Namespace) Function(name string, args ...reflect.Value) (string, error) { + parts := strings.Split(name, ".") + if len(parts) != 2 { + return "", fmt.Errorf("invalid function name: %s. Function names must be of the form fileName.FunctionName", name) + } + + mFuncs, ok := ns.funcs[parts[0]] + if !ok { + return "", fmt.Errorf("the function file named %s has not been loaded", parts[0]) + } + + fn, ok := mFuncs[parts[1]] + if !ok { + return "", fmt.Errorf("the function named %s does not exist in %s", parts[1], parts[0]) + } + + return fn(args...) +} + +func LoadFunctionFiles(funcsPath string) (*Namespace, error) { + tsPaths, err := filepath.Glob(path.Join(funcsPath, "*.ts")) + if err != nil { + return nil, err + } + + ns := &Namespace{ + funcs: make(map[string]map[string]func(...reflect.Value) (string, error)), + } + + for _, tsPath := range tsPaths { + mName := strings.TrimSuffix(path.Base(tsPath), ".ts") + funcs, warns := loadTsFunctionsFile(tsPath) + + for _, warn := range warns { + helpers.DistinctWarnLog.Warnf("Issue loading functions from %s: %v\n", tsPath, warn) + } + + if len(funcs) == 0 { + continue + } + + ns.funcs[mName] = make(map[string]func(...reflect.Value) (string, error)) + + for _, fn := range funcs { + ns.funcs[mName][fn.Name] = fn.Func + helpers.DistinctWarnLog.Infof("Loaded function: %s.%s\n", mName, fn.Name) + } + } + + return ns, nil +} + +func loadTsFunctionsFile(tsPath string) ([]FunctionDetails, []error) { + vm, exports, err := executeTS(tsPath) + if err != nil { + return nil, []error{err} + } + + return extractFunctions(vm, exports) +} + +func executeTS(tsPath string) (*goja.Runtime, goja.Value, error) { + ctx, cancel := context.WithTimeout(context.Background(), maxFuncFileLoadTime) + defer cancel() + + tsFile, err := os.Open(tsPath) + if err != nil { + return nil, nil, err + } + + var vm *goja.Runtime + exportRuntime := func(cfg *typescript.EvaluateConfig) { vm = cfg.Runtime } + + _, err = typescript.EvaluateCtx(ctx, tsFile, + typescript.WithTranspile(), + typescript.WithAlmondModuleLoader(), + exportRuntime, + ) + if err != nil { + return nil, nil, fmt.Errorf("could not evaluate function file: %v", err) + } + + exports, err := vm.RunString("exports") + if err != nil { + return nil, nil, fmt.Errorf("the function file is missing exports: %v", err) + } + + return vm, exports, nil +} + +func extractFunctions(vm *goja.Runtime, exports goja.Value) ([]FunctionDetails, []error) { + var funcs []FunctionDetails + var warns []error + + exportMap, ok := exports.Export().(map[string]any) + if !ok { + return nil, []error{fmt.Errorf("the function file's 'exports' variable is not a map")} + } + + for name, obj := range exportMap { + fn, ok := obj.(func(goja.FunctionCall) goja.Value) + if !ok { + // Only looking for exported functions + continue + } + + detail := FunctionDetails{ + Name: name, + Func: func(args ...reflect.Value) (string, error) { + valueArgs := make([]goja.Value, len(args)) + for i, arg := range args { + val, err := toGojaType(vm, arg) + if err != nil { + return "", err + } + valueArgs[i] = val + } + + val := fn(goja.FunctionCall{Arguments: valueArgs}) + + var out string + err := vm.ExportTo(val, &out) + return out, err + }, + } + + if obj, ok := exportMap[name+"Examples"]; ok { + examples, err := extractExamples(obj.([]interface{})) + if err == nil { + detail.Examples = examples + } else { + warns = append(warns, err) + } + } + + funcs = append(funcs, detail) + } + + return funcs, warns +} + +func extractExamples(ifaces []interface{}) ([][2]string, error) { + var examples [][2]string + + _ = ifaces + + return examples, nil +} + +func toGojaType(vm *goja.Runtime, val reflect.Value) (goja.Value, error) { + iface := val.Interface() + + if t, ok := iface.(time.Time); ok { + return vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(t.UnixNano()/1e6)) + } + + return vm.ToValue(val.Interface()), nil +} diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go index 97d1b40ddbc..309d59b7d76 100644 --- a/tpl/tplimpl/template_funcs.go +++ b/tpl/tplimpl/template_funcs.go @@ -41,6 +41,7 @@ import ( _ "github.com/gohugoio/hugo/tpl/debug" _ "github.com/gohugoio/hugo/tpl/diagrams" _ "github.com/gohugoio/hugo/tpl/encoding" + _ "github.com/gohugoio/hugo/tpl/external" _ "github.com/gohugoio/hugo/tpl/fmt" _ "github.com/gohugoio/hugo/tpl/hugo" _ "github.com/gohugoio/hugo/tpl/images"